vlambda博客
学习文章列表

JDK自带的观察者模式源码分析以及和自定义实现的取舍

JDK自带的观察者模式源码分析以及和自定义实现的取舍


目录

结论在前,分析在后

java.util.Observer接口源码分析

  • 借鉴 JDK 封装方法的过多参数的方案

java.util.Observable 类源码分析

  • setChanged 这个开关的作用——可以借鉴思想

  • 备忘录模式的简单应用——实现无锁的线程安全

JDK观察者模式API的一些弊端

本篇文章大概2500字,阅读时间大约10分钟


在文章中,从各个维度总结了观察者设计模式,它有两大类,pull模式和push模式,需要熟练掌握。


下面总结一下JDK8默认提供的一套观察者模式的实现API。看看它究竟是怎么实现的,以及为何鲜有人直接在项目里使用。

01

太长不看版


结论:不推荐使用JDK自带的观察者模式API,而是自定义实现,但是可以借鉴其好的思想,当然大部分情况根本不需要我们自己实现,直接使用现成的异步框架,或者一些框架里提供的异步机制即可。



02

java.util.Observer接口源码分析


该接口十分简单,是各个观察者需要实现的接口,来自java.util包:

package java.util;
public interface Observer { void update(Observable o, Object arg);}

设计的十分直接,就是使用顶级父类 Object 做参数类型,然后自己可以定义一个参数封装类。另外,第一个参数 Observable 就是所谓的主题接口,JDK 也给默认实现了,但实现的不是特别好,估计 Oracle 也懒得改了。


这里有两点值得我们学习:

1、使用一个工具类封装散落的参数,提高易用性和可扩展性

2、加上这个Observable非常好,因为能让观察者知道到底是哪个主题通知的“我”


03

java.util.Observable类源码分析


这是该API设计最大的问题——使用的是类,而不是接口。如下是该类源码,删除了大量英文注释,改为更精简的形式,并加入了详尽的批注:

import java.util.Observer;import java.util.Vector;
/** * 这里其实就是主题接口的角色,只不过 JDK 的实现很烂——竟然用类封装的,这是公认的槽点之一。 */public class Observable { private boolean changed = false; private Vector obs; // 看到这里,其实也知道,这个API不仅太古老,而且还没人维护了,用的还是最老的,被淘汰的 Vector 实现的动态数组
public Observable() { obs = new Vector(); }
// 注册观察者,线程安全,这是优点之一,可以借鉴 public synchronized void addObserver(Observer o) { if (o == null) // 提高代码健壮性 throw new NullPointerException(); if (!obs.contains(o)) { // 注册时会去重,自定义实现需要注意也去重 obs.addElement(o); } }
// 观察者取消注册 public synchronized void deleteObserver(Observer o) { obs.removeElement(o); }
// 基于拉模型的通知方法 public void notifyObservers() { notifyObservers(null); }
// 基于推模型 public void notifyObservers(Object arg) { // 一个临时数组,用于并发访问被观察者时,保存观察者列表的当前状态——这就是基于备忘录模式的简单应用。 Object[] arrLocal; // 在获取到观察者列表之前,不允许其他线程改变观察者列表 synchronized (this) { if (!changed) return; arrLocal = obs.toArray(); // 重置变化标记位为 false clearChanged(); }
// 主题类释放锁,但是并不影响线程安全,因为加锁之前已经将观察者列表复制到临时数组 arrLocal // 在通知时我们只通知数组中的观察者,当前删除和添加观察者,都不会影响我们通知的对象 for (int i = arrLocal.length - 1; i >= 0; i--) ((Observer) arrLocal[i]).update(this, arg); }
public synchronized void deleteObservers() { obs.removeAllElements(); }
protected synchronized void setChanged() { changed = true; }
protected synchronized void clearChanged() { changed = false; }
public synchronized boolean hasChanged() { return changed; }
public synchronized int countObservers() { return obs.size(); }}

setChanged 这个开关的作用——可以借鉴设计思想

1、使得主题具备了很大的伸缩性

假如没有 setChanged,那么一旦主题的状态变了,就不得不立即通知订阅者,这不是很合理,需要一个缓冲——setChanged,如JDK一样,在notify方法中做判断,如果状态的变化达到了一个阈值,在设置 setChanged 条件,这时候才会真的通知,这个条件以及阈值的设置可以在主题类(继承了Observalbe类)的业务代码中实现。JDK还提供了配套的检查该标志的方法。

2、能筛选订阅者

3、能实现通知的撤销

主题中可以设置很多次的 setChanged,比如在一个事务中,在最后由于某种原因,事务失败,那么通知也必须取消,此时可以使用 clearChanged 方法轻松解决问题

4、主题的主动权控制

setChanged 和 clearChanged 方法均为 protected,而 notifyObservers 方法为 public,这就导致存在外部随意调用 notifyObservers 的可能,但是外部无法调用 setChanged,因此真正的控制权属于主题——即使外部能调用主题的通知方法,也是然并卵的


备忘录模式的简单应用——实现无锁的线程安全

如下源代码片段:

 // 基于推模型 public void notifyObservers(Object arg) { // 一个临时数组,用于并发访问被观察者时,保存观察者列表的当前状态——这就是基于备忘录模式的简单应用。 Object[] arrLocal; // 在获取到观察者列表之前,不允许其他线程改变观察者列表 synchronized (this) { if (!changed) return; arrLocal = obs.toArray(); // 重置变化标记位为 false clearChanged(); }
// 主题类释放锁,但是并不影响线程安全,因为加锁之前已经将观察者列表复制到临时数组 arrLocal // 在通知时我们只通知数组中的观察者,当前删除和添加观察者,都不会影响我们通知的对象 for (int i = arrLocal.length - 1; i >= 0; i--) ((Observer) arrLocal[i]).update(this, arg); }

主题类即使在清理了状态位之后就释放锁,也不影响通知方法的线程安全性,因为加锁之前已经将观察者列表复制到了一个临时数组 arrLocal——数组是不可变的,局部的,故也是线程安全的。在释放锁后通知观察者们,只是通知的该临时数组中保存的观察者的快照,在通知的时候即使有观察者被删除和新的观察者注册,也不会影响通知的过程正确。



04

JDK提供的观察者模式弊端总结


1、通知方法中的缺陷

看上面的源码可以发现一个问题,update 是观察者接口中的方法,是各个具体的观察者需要实现的方法,如果具体观察者的 update 方法有机会抛出异常,那么 RD 没有捕获,就会把异常抛出,导致整个通知过程失败,这里也是为什么不推荐使用该接口。


在自己实现的时候,可以把观察者的 update 方法用异常控制块包起来,保证通知过程能完整执行。


2、OOM 的隐患

JDK的主题也持有了观察者的引用,如果未正常处理——及时的从主题中删除废弃的观察者,会导致大量的废弃观察者无法被回收。这里其实主要还是业务代码的问题。如果观察者具体实现代码有问题,可能会导致主题和观察者对象形成循环引用,在某些采用引用计数的垃圾回收器可能导致无法回收。但是现代GC中,这种问题不会出现,引用计数器算法早已经被放弃使用。


3、持有观察者的集合类 Vector 的性能问题

先说结论——Vector是最旧的 List 实现,不再被JDK推荐使用。这是一个槽点,当初实现 JDK 的观察者 API 的时候,可能动态数组用 vector 实现比较好,但是现在早就是推荐使用 Arraylist 了。虽然vector 与 ArrayList功能相似,但是:

1、Vector 是线程安全的list集合,Vector 完全基于 synchronized 实现同步,虽然它的操作与 ArrayList 几乎一样,但是很多时候我们不需要那么重的实现,毕竟加锁会影响性能。故一般直接使用ArrayList,而且,一定要实现线程安全的动态数组,也轮不到用 Vector,可以使用 JUC 中的容器—— CopyOnWriteArraylist 等,或者用 Collections 类的同步 List 静态方法来转换为同步List

2、Vector 的部分方法名太长,ArrayList 的对应实现方法名短些,便于阅读,目前仍在使用 Vector 的软件,基本都是为了兼容旧库和懒得改

3、Vector 的容量增长性能很差,Vector 是可变数组,初始 length 是 10 ,如果超过 length 时,会以 100% 比率增长 length,即变成20,所以存在内存浪费的现象,而 Arraylist 的 length 是以 50% 比率增加,所以相比来说,内存使用率较高


4、主题通知观察者的顺序很奇葩,有bug风险

看源码得知,主题通知观察者的顺序是 tmd 的倒叙,导致通知观察者的顺序和注册的顺序不一样,如果业务代码对顺序有要求,就不好弄了


5、主题是类实现的,扩展难

众所周知,Java 没有多继承机制,如果具体主题除了继承主题类外, 还想继承其他业务类,就没法儿写了。典型的违背了“组合(聚合)优于继承的”设计原则。故一般自定义的实现比较多,也不难。虽然 JDK 给我们做了封装,但是很多时候业务需求复杂,JDK 的 API 并不能满足我们的需求。 


END


阅读原文,获得更多精彩内容