通过实现观察者模式来提供 Java 事件通知(Java event notification)似乎不是件什么难事儿,但这过程中也很容易就掉进一些陷阱。本文介绍了我自己在各种情形下,不小心制造的一些常见错误。

Java 事件通知

让我们从一个最简单的 Java Bean 开始,它叫StateHolder,里面封装了一个私有的 int 型属性 state 和常见的访问方法:

public class StateHolder {

private int state;

public int getState() {

return state;

}

public void setState( int state ) {

this.state = state;

}

}

现在假设我们决定要 Java bean 给已注册的观察者广播一条 状态已改变 事件。小菜一碟!!!定义一个最简单的事件和监听器简直撸起袖子就来……

// change event to broadcast

public class StateEvent {

public final int oldState;

public final int newState;

StateEvent( int oldState, int newState ) {

this.oldState = oldState;

this.newState = newState;

}

}

// observer interface

public interface StateListener {

void stateChanged( StateEvent event );

}

接下来,我们需要在 StateHolder 的实例里注册 StatListeners。

public class StateHolder {

private final Set listeners = new HashSet<>();

[...]

public void addStateListener( StateListener listener ) {

listeners.add( listener );

}

public void removeStateListener( StateListener listener ) {

listeners.remove( listener );

}

}

最后一个要点,需要调整一下StateHolder#setState这个方法,来确保每次状态有变时发出的通知,都代表这个状态真的相对于上次产生变化了:

public void setState( int state ) {

int oldState = this.state;

this.state = state;

if( oldState != state ) {

broadcast( new StateEvent( oldState, state ) );

}

}

private void broadcast( StateEvent stateEvent ) {

for( StateListener listener : listeners ) {

listener.stateChanged( stateEvent );

}

}

搞定了!要的就是这些。为了显得专(zhuang)业(bi)一点,我们可能还甚至为此实现了测试驱动,并为严密的代码覆盖率和那根表示测试通过的小绿条而洋洋自得。而且不管怎么样,这不就是我从网上那些教程里面学来的写法吗?

那么问题来了:这个解决办法是有缺陷的……

并发修改

像上面那样写 StateHolder 很容易遇到并发修改异常(ConcurrentModificationException),即使仅仅限制在一个单线程里面用也不例外。但究竟是谁导致了这个异常,它又为什么会发生呢?

java.util.ConcurrentModificationException

at java.util.HashMap$HashIterator.nextNode(HashMap.java:1429)

at java.util.HashMap$KeyIterator.next(HashMap.java:1453)

at com.codeaffine.events.StateProvider.broadcast(StateProvider.java:60)

at com.codeaffine.events.StateProvider.setState(StateProvider.java:55)

at com.codeaffine.events.StateProvider.main(StateProvider.java:122)

乍一看这个错误堆栈包含的信息,异常是由我们用到的一个 HashMap 的 Iterator 抛出的,可在我们的代码里没有用到任何的迭代器,不是吗?好吧,其实我们用到了。要知道,写在 broadcast 方法里的 for each 结构,实际上在编译时是会被转变成一个迭代循环的。

因为在事件广播过程中,如果一个监听器试图从 StateHolder 实例里面把自己移除,就有可能导致 ConcurrentModificationException。所以比起在原先的数据结构上进行操作,有一个解决办法就是我们可以在这组监听器的快照(snapshot)上进行迭代循环。

这样一来,“移除监听器”这一操作就不会再干扰事件广播机制了(但要注意的是通知还是会有轻微的语义变化,因为当 broadcast 方法被执行的时候,这样的移除操作并不会被快照体现出来):

private void broadcast( StateEvent stateEvent ) {

Set snapshot = new HashSet<>( listeners );

for( StateListener listener : snapshot ) {

listener.stateChanged( stateEvent );

}

}

但是,如果 StateHolder 被用在一个多线程的环境里呢?

同步

要再多线程的环境里使用 StateHolder ,它就必须是线程安全的。不过这也很容易实现,给我们类里面的每个方法加上 synchronized 就搞定了,不是吗?

public class StateHolder {

public synchronized void addStateListener( StateListener listener ) { [...]

public synchronized void removeStateListener( StateListener listener ) { [...]

public synchronized int getState() { [...]

public synchronized void setState( int state ) { [...]

现在我们读写操作 一个 StateHolder 实例的时候都有了内置锁(Intrinsic Lock) 做保证,这使得公有方法具有了原子性,也确保了正确的状态对不同的线程都可见。任务完成!

才怪……尽管这样的实现是线程安全的,但一旦程序要调用它,就需要承担死锁的风险。

设想一下如下这种情形:线程 A 改变了 StateHolder 的状态 S,在向各个监听器(listener)广播这个状态 S 的时候,线程 B 视图访问状态 S ,然后被阻塞。如果 B 持有了一个对象的同步锁,这个对象又是关于状态 S的,并且本来是要广播给众多监听器当中的某一个的,这种情况下我们就会遇到一个死锁。

这就是为什么我们要缩小状态访问的同步性,在一个“保护通道”里面来广播这个事件:

public class StateHolder {

private final Set listeners = new HashSet<>();

private int state;

public void addStateListener( StateListener listener ) {

synchronized( listeners ) {

listeners.add( listener );

}

}

public void removeStateListener( StateListener listener ) {

synchronized( listeners ) {

listeners.remove( listener );

}

}

public int getState() {

synchronized( listeners ) {

return state;

}

}

public void setState( int state ) {

int oldState = this.state;

synchronized( listeners ) {

this.state = state;

}

if( oldState != state ) {

broadcast( new StateEvent( oldState, state ) );

}

}

private void broadcast( StateEvent stateEvent ) {

Set snapshot;

synchronized( listeners ) {

snapshot = new HashSet<>( listeners );

}

for( StateListener listener : snapshot ) {

listener.stateChanged( stateEvent );

}

}

}

上面这段代码是在之前的基础上稍加改进来实现的,通过使用 Set 实例作为内部锁来提供合适(但也有些过时)的同步性,监听者的通知事件在保护块之外发生,这样就避免了一种死等的可能。

注意: 由于系统并发操作的天性,这个解决方案并不能保证变化通知按照他们产生的顺序依次到达监听器。如果观察者一侧对实际状态的准确性有较高要求,可以考虑把 StateHolder 作为你事件对象的来源。

如果事件顺序这在你的程序里显得至关重要,有一个办法就是可以考虑用一个线程安全的先入先出(FIFO)结构,连同监听器的快照一起,在 setState 方法的保护块里缓冲你的对象。只要 FIFO 结构不是空的,一个独立的线程就可以从一个不受保护的区域块里触发实际事件(生产者-消费者模式),这样理论上就可以不必冒着死锁的危险还能确保一切按照时间顺序进行。我说理论上,是因为到目前为止我也还没亲自这么试过。。

鉴于前面已经实现的,我们可以用诸如 CopyOnWriteArraySet 和 AtomicInteger 来写我们的这个线程安全类,从而使这个解决方案不至于那么复杂:

public class StateHolder {

private final Set listeners = new CopyOnWriteArraySet<>();

private final AtomicInteger state = new AtomicInteger();

public void addStateListener( StateListener listener ) {

listeners.add( listener );

}

public void removeStateListener( StateListener listener ) {

listeners.remove( listener );

}

public int getState() {

return state.get();

}

public void setState( int state ) {

int oldState = this.state.getAndSet( state );

if( oldState != state ) {

broadcast( new StateEvent( oldState, state ) );

}

}

private void broadcast( StateEvent stateEvent ) {

for( StateListener listener : listeners ) {

listener.stateChanged( stateEvent );

}

}

}

既然 CopyOnWriteArraySet 和 AtomicInteger 已经是线程安全的了,我们不再需要上面提到的那样一个“保护块”。但是等一下!我们刚刚不是在学到应该用一个快照来广播事件,来替代用一个隐形的迭代器在原集合(Set)里面做循环嘛?

这或许有些绕脑子,但是由 CopyOnWriteArraySet 提供的 Iterator(迭代器)里面已经有了一个“快照“。CopyOnWriteXXX 这样的集合就是被特别设计在这种情况下大显身手的——它在小长度的场景下会很高效,而针对频繁迭代和只有少量内容修改的场景也做了优化。这就意味着我们的代码是安全的。

随着 Java 8 的发布,broadcast 方法可以因为Iterable#forEach 和 lambdas表达式的结合使用而变得更加简洁,代码当然也是同样安全,因为迭代依然表现为在“快照”中进行:

private void broadcast( StateEvent stateEvent ) {

listeners.forEach( listener -> listener.stateChanged( stateEvent ) );

}

异常处理

本文的最后介绍了如何处理抛出 RuntimeExceptions 的那些损坏的监听器。尽管我总是严格对待 fail-fast 错误机制,但在这种情况下让这个异常得不到处理是不合适的。尤其考虑到这种实现经常在一些多线程环境里被用到。

损坏的监听器会有两种方式来破坏系统:第一,它会阻止通知向观察者的传达过程;第二,它会伤害那些没有准备处理好这类问题的调用线程。总而言之它能够导致多种莫名其妙的故障,并且有的还难以追溯其原因,

因此,把每一个通知区域用一个 try-catch 块来保护起来会显得比较有用。

private void broadcast( StateEvent stateEvent ) {

listeners.forEach( listener -> notifySafely( stateEvent, listener ) );

}

private void notifySafely( StateEvent stateEvent, StateListener listener ) {

try {

listener.stateChanged( stateEvent );

} catch( RuntimeException unexpected ) {

// appropriate exception handling goes here...

}

}

总结

综上所述,Java 的事件通知里面有一些基本要点你还是必须得记住的。在事件通知过程中,要确保在监听器集合的快照里做迭代,保证事件通知在同步块之外,并且在合适的时候再安全地通知监听器。

但愿我写的这些让你觉得通俗易懂,最起码尤其在并发这一节不要再被搞得一头雾水。如果你发现了文章中的错误或者有其它的点子想分享,尽管在文章下面的评论里告诉我吧。

原文链接: javacodegeeks 翻译: ImportNew.com - 林申

译文链接: www.importnew.com/15446.html

[ 转载请保留原文出处、译者和译文链接。]

java后端站内通知_正确使用Java事件通知相关推荐

  1. java 事件通知_正确获取Java事件通知

    java 事件通知 实现观察者模式以提供Java事件通知似乎是一件容易的事. 但是,容易陷入一些陷阱. 这是我在各种场合不慎造成的常见错误的解释-- Java事件通知 让我们从一个简单的bean St ...

  2. java实现站内搜索

    1.站内搜索 在以往的网站建设,企业系统的搭建过程中,因为信息比较简单,比较少,站内搜索可能不是必要的选项,而今,时代的发展, 信息量的增大,网站逻辑的复杂,企业自身对信息架构.管理.发布的需求,以及 ...

  3. java 后端开发好吗_后端开发学Java好还是学c++好呢?

    C++与 java 的抉择 为了找工作:选Java. 为挑战自我:选C++. 很多人都说会C++就能会快掌握Jave.C++是不好学,但是我告诉你java也不好学.C++难是难在语言本身,java难是 ...

  4. java 面试题合集_撩课-Java面试题合辑1-50题

    1.简述JDK.JRE.JVM? 一.JDK JDK(Java Development Kit) 是整个JAVA的核心, 包括了Java运行环境(Java Runtime Envirnment), 一 ...

  5. Java 并发编程解析 , 如何正确理解Java领域中的内存模型

    这些年,随着CPU.内存.I/O 设备都在不断迭代,不断朝着更快的方向努力.在这个快速发展的过程中,有一个核心矛盾一直存在,就是这三者的速度差异.CPU 和内存的速度差异可以形象地描述为:CPU 是天 ...

  6. 深入java虚拟机 第四版_深入理解Java虚拟机-常用vm参数分析

    Java虚拟机深入理解系列全部文章更新中... https://blog.ouyangsihai.cn/shen-ru-li-jie-java-xu-ni-ji-java-nei-cun-qu-yu- ...

  7. java map按照value排序_基础:Java集合需要注意的 5 个问题

    点击上方 Java后端,选择 设为星标 优质文章,及时送达 Java集合中的List.Set和Map作为Java集合食物链的顶级,可谓是各有千秋.本文将对于List.Set和Map之间的联系与区别进行 ...

  8. java文章管理系统源码_融成Java后台网站内容管理系统 v3.2.1

    融成Java后台网站内容管理系统是一款基于Java语言开发的功能强大的内容管理系统.成功实现了既能够管理包括企业官网.门户站点.图片视频软件等上传下载网站.博客网站.电商购物网站.物流管理网站等复杂多 ...

  9. Java中内部做监视器_监视器模式 java

    广告 精选中小企业最主流配置,适用于web应用场景.小程序及简单移动App,所有机型免费分配公网IP和50G高性能云硬盘(系统盘). mutex实际上就是对象本身 } 复制代码什么是监视器模式 jav ...

最新文章

  1. linux nat 日志,IPtables日志管理  (记录NAT信息)
  2. C语言打印文件数据,用C语言输出文件内所有数据
  3. 如何使用应用日志(Application Log)
  4. 深入分析Ribbon源码分析
  5. 如何强制.NET应用程序以管理员身份运行?
  6. MongoDB 概述、下载安装、配置 、启动与连接
  7. oracle用户删除了可以恢复吗,恢复用户误删除的没备份的Oracle数据文件
  8. 《惢客创业日记》2019.05.20(周一)向技术大牛请教(二)
  9. mid制作乐谱_作品1:MIDI彩虹钢琴(将简谱或五线谱制作成mid)
  10. 黑客帝国角色 之 尼奥解读
  11. python求解方程组_NumPy线性方程组求解
  12. 七年级画图计算机教案,信息技术画图软件学习教案
  13. YY前端HTML规范
  14. 矩阵求导公式的推导和计算(公式推导法)
  15. iOS12加密相册、保险箱打开就闪退的,关闭4g和WiFi即可正常使用
  16. 图。Dijkstra标号算法(最短路径)
  17. 未来的智能制造,或许会往这些方向推进
  18. 浏览器开发者选项取消已在调试程序中暂停
  19. 紫薯第10章数学 kaungbin专题14数论基础
  20. 一点点有的没的和一年总结

热门文章

  1. CS229——NODE1part1
  2. 华为云苏光牛:生态建设是数据库产业发展非常重要的一环
  3. 一步一步配置DataGuard Broker
  4. 干货分享 | 史上最全Oracle体系结构整理
  5. 遇见未来 | MongoDB增强事务支持,向NewSQL的方向迈进
  6. 带你掌握4种Python 排序算法
  7. 一文教会你认识Vuex状态机
  8. python编译反编译,你不知道的心机与陷阱
  9. 【华为云技术分享】当我们在谈论卡片时,我们到底在谈论什么?
  10. 【华为云技术分享】Linux内核源码结构(1)