观察者模式中订阅和取消订阅的生命周期如何管理?
简化版
观察者模式必须管理订阅生命周期:谁负责注册、何时取消、重复订阅怎么办、对象销毁后是否还被引用。本地观察者通常由事件源维护监听器列表,注册和取消要成对出现;长生命周期事件源引用短生命周期观察者时,尤其容易内存泄漏。工程上要提供明确的 subscribe/unsubscribe,防重复注册,并在组件关闭、页面卸载或 Bean 销毁时清理。
详细版
订阅管理看似简单:
subject.addObserver(observer);
subject.removeObserver(observer);
但真实项目里会遇到:
- 同一个观察者被重复注册;
- 对象已经不使用但没有取消订阅;
- 事件源生命周期比观察者更长;
- 多线程同时订阅和通知;
- 动态插件加载后没有卸载监听器。
如果不处理,轻则重复执行,重则内存泄漏和脏回调。
记忆钩子:观察者模式不是只管通知,还要管订阅从哪里来、什么时候走。
完整版教学
面试提示:订阅生命周期题要突出“注册、注销、弱引用、作用域、并发安全”这 5 个关键词,尤其别漏内存泄漏。
一、订阅生命周期为什么重要
观察者模式有一个基本结构:
Subject
observers: [A, B, C]
只要 Subject 持有 Observer 的强引用,Observer 就不会被垃圾回收。如果 Subject 是全局单例,而 Observer 是某个页面、会话或插件对象,就很容易泄漏。
二、注册和取消要成对设计
接口通常包含:
interface Subject {
void subscribe(Observer observer);
void unsubscribe(Observer observer);
void notify(Event event);
}
调用方在生命周期开始时注册,在结束时取消:
component.start() -> subscribe
component.stop() -> unsubscribe
前端页面、桌面 UI、插件系统、长连接会话都特别需要这个意识。
三、要防止重复订阅
如果使用 List 保存观察者,同一个对象可能注册多次:
observers.add(observer);
observers.add(observer);
一次事件会执行两遍。可以用 Set 或注册前检查:
private final Set<Observer> observers = ConcurrentHashMap.newKeySet();
public void subscribe(Observer observer) {
observers.add(observer);
}
但要注意对象的 equals/hashCode 是否符合预期。也可以用 listenerName 做唯一 key。
四、长生命周期引用短生命周期最危险
典型风险:
| 事件源 | 观察者 | 风险 |
|---|---|---|
| 全局 EventBus | 页面组件 | 页面关闭后仍被引用 |
| Spring singleton | request 对象 | 请求结束后仍回调 |
| 静态 Subject | 插件实例 | 插件卸载后无法释放 |
| 定时器 | 临时任务对象 | 任务结束后仍保留 |
核心判断方法:谁活得更久。如果事件源更长寿,就必须特别关注取消订阅。
五、可以使用订阅句柄降低误用
订阅时返回一个句柄:
interface Subscription {
void cancel();
}
Subscription subscription = eventBus.subscribe(event -> handle(event));
subscription.cancel();
这样调用方不需要记住用哪个 observer 对象去取消,生命周期管理更清晰。
六、弱引用不是万能方案
有些框架会用弱引用保存观察者,减少内存泄漏。但弱引用也有代价:
- 观察者可能被提前回收;
- 调试更困难;
- 仍然需要清理失效引用;
- 不能替代明确生命周期。
弱引用适合特定框架设计,不是业务代码逃避取消订阅的万能答案。
七、并发场景要保护观察者列表
如果通知时有人取消订阅,普通 ArrayList 可能抛异常或漏通知。常见方案:
private final CopyOnWriteArrayList<Observer> observers = new CopyOnWriteArrayList<>();
适合读多写少的监听器列表。若订阅变更很频繁,可能要用锁或快照复制。
八、常见误区与追问
- 误区:注册监听器后不用管,JVM 会自动清理。 只要事件源还强引用观察者,观察者就不会被回收。
- 误区:重复订阅没有影响。 可能导致短信发两次、积分加两次、日志重复写。
- 误区:只要用 Spring Bean 就没有生命周期问题。 单例 Bean 持有短生命周期对象仍然会出问题。
- 误区:弱引用可以替代取消订阅。 弱引用有适用边界,业务生命周期仍要明确。
- 追问:如何防重复注册? 使用 Set、listenerName 唯一索引、订阅前检查或返回可取消句柄。
- 追问:什么时候取消订阅? 页面卸载、会话关闭、插件卸载、组件 stop、Bean destroy 时都要清理。
- 追问:通知时取消订阅怎么办? 使用 CopyOnWriteArrayList、快照遍历或加锁保护观察者列表。
九、加强记忆
- 观察者模式要同时管理订阅和通知。
- 注册和取消要成对出现。
- 长生命周期事件源引用短生命周期观察者最危险。
- 重复订阅会导致重复执行。
- 订阅句柄能简化取消逻辑。
- 并发通知时要保护观察者列表。