← 返回题目列表

观察者模式有哪些线程安全和内存泄漏问题?

高频 困难 第 16 / 27 题 更新于 2026/07/28
观察者模式线程安全内存泄漏并发

简化版

观察者模式的线程安全问题主要来自观察者列表并发注册、移除和通知;内存泄漏问题主要来自观察者注册后没有取消订阅,导致被主题长期引用无法回收。解决思路是使用线程安全集合、通知时快照复制、明确取消订阅、弱引用或生命周期管理。

详细版

观察者模式常见并发问题:

  1. 通知时另一个线程添加或删除观察者,导致并发修改异常;
  2. 多线程同时注册造成观察者列表状态不一致;
  3. 观察者执行慢或阻塞,影响其他观察者;
  4. 观察者抛异常导致通知链中断;
  5. 异步观察者共享状态引发竞态。

常见内存泄漏问题:

  • Subject 持有 Observer 强引用;
  • Observer 生命周期本该结束,但没有取消注册;
  • Subject 生命周期更长,导致 Observer 无法被 GC。

解决方式包括:

  • 使用 CopyOnWriteArrayList 等线程安全集合;
  • 通知前复制观察者快照;
  • 提供并调用 detach/unsubscribe
  • 对短生命周期观察者使用弱引用;
  • 在组件销毁时统一取消订阅。

完整版教学

一、为什么观察者模式容易出并发问题

观察者模式通常会维护一个观察者列表:

private final List<Observer> observers = new ArrayList<>();

如果多个线程同时操作这个列表,就会有风险。

例如一个线程正在遍历通知:

for (Observer observer : observers) {
    observer.update(event);
}

另一个线程同时添加或删除观察者,就可能出现 ConcurrentModificationException,或者出现通知遗漏、重复通知等问题。

二、观察者列表要考虑线程安全

常见处理方式有几种。

第一,使用线程安全集合:

private final List<Observer> observers = new CopyOnWriteArrayList<>();

CopyOnWriteArrayList 适合读多写少的场景。观察者注册和移除不频繁,通知遍历很频繁时,它很合适。

第二,通知前复制快照:

List<Observer> snapshot;
synchronized (this) {
    snapshot = new ArrayList<>(observers);
}
for (Observer observer : snapshot) {
    observer.update(event);
}

这样通知过程中即使原列表变化,也不会影响当前遍历。

三、观察者异常要隔离

如果一个观察者抛异常,是否要影响其他观察者?这是设计时必须明确的问题。

如果直接这样写:

for (Observer observer : observers) {
    observer.update(event);
}

第一个观察者抛异常,后面的观察者可能都不会执行。

更稳的做法是按业务需求隔离异常:

for (Observer observer : observers) {
    try {
        observer.update(event);
    } catch (Exception e) {
        log.error("observer failed", e);
    }
}

当然,如果某些观察者失败必须阻断主流程,也可以让异常继续抛出。关键是要有明确语义。

四、内存泄漏来自长期强引用

观察者模式的内存泄漏很经典。

假设一个长生命周期的 Subject 持有短生命周期 Observer:

全局事件源 -> 页面组件监听器

页面组件关闭后,如果没有取消监听,全局事件源仍然引用它。垃圾回收器会认为它还可达,于是无法回收。

这在 GUI、移动端、前端事件监听、长生命周期服务中很常见。

解决方法:

  • 在组件销毁时取消订阅;
  • 使用弱引用保存观察者;
  • 由容器统一管理监听器生命周期;
  • 避免匿名监听器无法移除。

五、异步观察者还要注意共享状态

如果观察者异步执行,问题会更多。

例如多个观察者共享同一个可变对象,或者同一个观察者被多个事件并发调用,都可能产生竞态条件。

解决方式包括:

  • 事件对象尽量不可变;
  • 观察者内部不要保存请求级状态;
  • 必要时加锁或使用线程安全结构;
  • 不同类型任务使用隔离线程池;
  • 对重复事件做幂等处理。

六、订阅列表的并发与生命周期

Subject 持有 1000 个监听器,每个连带引用 100 KB 对象图,就可能阻止约 100 MB 内存回收。

register/unregister <-> observer list snapshot -> notify; strong reference -> retained graph

这个推演把模式带来的收益和成本放在同一条调用链上。设计评审时既要确认结果正确,也要确认额外层次没有改变原有契约。

七、边界、代价与验证

检查维度应确认的内容
正确性最可靠的做法是返回可关闭订阅句柄,并由组件生命周期显式注销。
适用边界CopyOnWriteArrayList 适合读多写少,频繁订阅变更时复制成本高;弱引用能缓解滞留但可能让监听器意外消失。
测试证据覆盖正常路径、异常路径、空值或未知类型,并断言调用次数与顺序
运行成本记录额外对象、调用层数、延迟、线程或内存开销,不用模式名称代替测量

模式落地后要用单元测试验证独立职责,用集成测试验证对象装配和真实调用入口。涉及并发、远程或异步时,还要补充竞态、超时、重复执行与资源释放测试。

易错点:最可靠的做法是返回可关闭订阅句柄,并由组件生命周期显式注销。

八、常见误区与追问

  • 误区:把观察者改成弱引用就彻底解决泄漏。 弱引用改变存活语义且清理时机不确定,仍需移除失效引用和管理订阅生命周期。
  • 误区:发布事件天然等于异步和最终一致性。 观察者只定义一对多通知关系;同步还是异步、是否跨进程以及一致性语义都由具体实现决定。
  • 误区:示例代码能运行,就代表模式边界设计正确。 能运行只证明一条路径,仍要检查异常、并发、顺序、生命周期以及新增实现时是否修改稳定代码。
  • 追问:通知期间允许注销监听器吗? 可以用快照迭代或适当并发容器定义清晰语义,避免直接修改正在遍历的普通列表。
  • 追问:观察者失败后由谁重试? 模式本身没有答案;同步场景要定义异常传播,异步场景要由执行器、事件总线或消息系统提供重试与死信策略。
  • 追问:怎样验证这套设计没有改变原有语义? 先做基线行为测试,再对新增层验证返回值、异常类型、调用次数和顺序;性能敏感处还要比较改造前后的量化指标。

九、加强记忆

观察者模式的线程安全重点在“观察者列表并发修改”和“观察者执行隔离”;内存泄漏重点在“注册后忘记取消订阅”。工程上要用线程安全集合或快照遍历,组件销毁时取消监听,异步场景再补上幂等和状态隔离。