← 返回题目列表

观察者模式中订阅和取消订阅的生命周期如何管理?

高频 中等 第 9 / 27 题 更新于 2026/08/02
观察者模式订阅管理生命周期内存泄漏

简化版

观察者模式必须管理订阅生命周期:谁负责注册、何时取消、重复订阅怎么办、对象销毁后是否还被引用。本地观察者通常由事件源维护监听器列表,注册和取消要成对出现;长生命周期事件源引用短生命周期观察者时,尤其容易内存泄漏。工程上要提供明确的 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 singletonrequest 对象请求结束后仍回调
静态 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、快照遍历或加锁保护观察者列表。

九、加强记忆

  1. 观察者模式要同时管理订阅和通知。
  2. 注册和取消要成对出现。
  3. 长生命周期事件源引用短生命周期观察者最危险。
  4. 重复订阅会导致重复执行。
  5. 订阅句柄能简化取消逻辑。
  6. 并发通知时要保护观察者列表。