前言

总的结论就是:不推荐使用JDK自带的观察者API,而是自定义实现,但是可以借鉴其好的思想。

java.util.Observer 接口源码分析

该接口十分简单,是各个观察者需要实现的接口

package java.util;

public interface Observer {
void update(Observable o, Object arg);

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

也十分直接,就是使用顶级父类 Object 做参数类型,然后自己可以定义一个参数封装的类。

另外,第一个参数 Observable 就是所谓的主题接口,JDK 给实现了,但是实现的不是特别好。估计 Oracle 也懒得改了,这里加上这个参数,非常好,因为能让观察者知道到底是哪个主题通知的“我”。

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、能筛选订阅者

只有有效通知可以调用 setChanged。比如,微信朋友圈的一条状态,好友 A 点赞,后续该状态的点赞和评论并不是每条都通知 A,只有 A 的好友触发的操作才会通知A——好友才会调用setChanged,这个业务逻辑就可以借鉴JDK

3、能实现通知的撤销

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

4、主题的主动权控制

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

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

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

public void notifyObservers(Object arg) {
// 一个临时数组,用于并发访问被观察者时,保存观察者列表的当前状态——这就是基于备忘录模式的简单应用。
Object[] arrLocal;
// 在获取到观察者列表之前,不允许其他线程改变观察者列表
synchronized (this) {
if (!changed)
return;
arrLocal = obs.toArray();
// 重置变化标记位为 false
clearChanged();
} for (int i = arrLocal.length - 1; i >= 0; i--)
((Observer) arrLocal[i]).update(this, arg);
}

通知方法中的缺陷

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

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

OOM 的隐患(微不足道)

主题也持有了观察者的引用,如果未正常处理——及时的从主题中删除废弃的观察者,会导致大量的废弃观察者无法被回收。这里其实主要还是业务代码的问题。

如果观察者具体实现代码有问题,可能会导致主题和观察者对象形成循环引用,在某些采用引用计数的垃圾回收器可能导致无法回收。但是,现代GC中,这种问题不会出现,引用计数器算法早已经被放弃使用。

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

先说结论——Vector是最旧的 List 实现,不再推荐使用。

这又是一个槽点,当初实现 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% 比率增加,所以相比来说,内存使用率较高

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

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

主题是类实现的,扩展难

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

欢迎关注

dashuai的博客是终身学习践行者,大厂程序员,且专注于工作经验、学习笔记的分享和日常吐槽,包括但不限于互联网行业,附带分享一些PDF电子书,资料,帮忙内推,欢迎拍砖!

JDK 自带的观察者模式源码分析以及和自定义实现的取舍的更多相关文章

  1. JDK中String类的源码分析(二)

    1.startsWith(String prefix, int toffset)方法 包括startsWith(*),endsWith(*)方法,都是调用上述一个方法 public boolean s ...

  2. drf复习(一)--原生djangoCBV请求生命周期源码分析、drf自定义配置文件、drf请求生命周期dispatch源码分析

    admin后台注册model  一.原生djangoCBV请求生命周期源码分析 原生view的源码路径(django/views/generic/base.py) 1.从urls.py中as_view ...

  3. 并发编程之 ConcurrentHashMap(JDK 1.8) putVal 源码分析

    前言 我们之前分析了Hash的源码,主要是 put 方法.同时,我们知道,HashMap 在并发的时候是不安全的,为什么呢?因为当多个线程对 Map 进行扩容会导致链表成环.不单单是这个问题,当多个线 ...

  4. JDK之集合乱序源码分析

    在JAVA的JDK中Collections类提供了shuffle方法用来对给定的集合参数进行乱序重排,之前面试也被问到过类似的问题,看了一下JDK的源码实现做个记录 1. 方法签名: Collecti ...

  5. hadoop自带例子SecondarySort源码分析MapReduce原理

    这里分析MapReduce原理并没用WordCount,目前没用过hadoop也没接触过大数据,感觉,只是感觉,在项目中,如果真的用到了MapReduce那待排序的肯定会更加实用. 先贴上源码 pac ...

  6. MVC源码分析 - ModelBinder绑定 / 自定义数据绑定

    这几天老感觉不对, 总觉得少点什么, 今天才发现, 前面 3 里面, 在获取Action参数信息的时候,  少解析了. 里面还有一个比较重要的东西. 今天看也是一样的. 在 InvokeAction( ...

  7. Java工程师高薪训练营-第一阶段 开源框架源码解析-模块一 持久层框架涉及实现及MyBatis源码分析-任务一:自定义持久层框架

    目录 任务一:自定义持久层框架 1.1 JDBC回顾及问题分析 1.2 自定义持久层框架思路分析 1.3 IPersistence_Test编写 1.3.1 XXXMapper.xml详解 1.3.2 ...

  8. 【笔记】拉勾Java工程师高薪训练营-第一阶段 开源框架源码解析-模块一 持久层框架涉及实现及MyBatis源码分析-任务一:自定义持久层框架

    以下笔记是我看完视频之后总结整理的,部分较为基础的知识点也做了补充,如有问题欢迎沟通. 目录 任务一:自定义持久层框架 1.1 JDBC回顾及问题分析 1.2 自定义持久层框架思路分析 1.3 IPe ...

  9. JDK 1.8之 HashMap 源码分析

    转载请注明出处:http://blog.csdn.net/crazy1235/article/details/75579654 构造函数 Node hash put treeifyBin get re ...

随机推荐

  1. BZOJ3536 : [Usaco2014 Open]Cow Optics

    枚举最后光线射到终点的方向,求出从起点出发以及从终点出发的光路,扫描线+树状数组统计交点个数即可. 注意当光路成环时,对应的两个方向应该只算一次. 时间复杂度$O(n\log n)$. #includ ...

  2. C++学习笔记56:异常处理

    异常处理 异常处理的语法 抛掷异常的程序段 throw表达式: 捕获并处理异常的程序段 try 复合语句 catch(异常声明) 复合语句 catch(异常声明) 复合语句 注意:如果匹配的处理器没有 ...

  3. Source map error

    前端访问接口时火狐浏览器控制台出现了这个问题, source map文件是js文件压缩后,文件的变量名替换对应.变量所在位置等元信息数据文件,一般这种文件和min.js主文件放在同一个目录下. 比如压 ...

  4. yii2 数据库和ActiveRecord

    Yii2数据库和 ActiveRecord 类 1.在 common/config/main-local.php 里面配置数据账号和密码. 2.ActiveRecord(活动记录,简称AR类),提供了 ...

  5. unity游戏设计与实现 --读书笔记(一)

    1,  游戏入门的一些知识点,游戏对象GameObject(角色), 组件Compoent(角色的功能),资源Asset(美术素材呵呵音频等的数据),场景Scene(用以放置各个角色,负责展示画面), ...

  6. h5本地缓存(localStorage,sessionStorage)

    H5本地存储数据 localStorage,sessionStorage的区别: 相同点:  缓存数据比cookie的范围大; localStorage:关闭浏览器数据不会消失,除非手动删除数据 se ...

  7. windows下安装Mysql—图文详解

    mysql安装过程及注意事项: 1.1. 下载: 我下载的是64位系统的zip包: 下载地址:https://dev.mysql.com/downloads/mysql/ 下载zip的包: 下载后解压 ...

  8. 对学习Ajax的知识总结

    1.对Ajax的初步认识 1.1. Ajax 是一种网页开发技术,(Asynchronous Javascript + XML)异步 JavaScript 和 XML: 1.2.Ajax 是异步交互, ...

  9. js float运算精度问题

    先放个前辈的文章:JavaScript数字精度丢失问题总结 今天遇到了19.99*100的问题,答案不等于1999,因为在javascript中浮点数的计算是以2进制计算的.自己写了一波解决方法(不能 ...

  10. SpringMVC Controller之间的重定向和转发

    同一个controller之间重定向和转发 ①redirect 在Controller的映射方法中,其返回值改为:return "redirect:XXX"; ②forward 这 ...