正确使用Java事件通知

通过实现

Java 事件通知

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

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

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

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

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

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

并发修改

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

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

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

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

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

同步

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

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

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

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

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

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

注意: 由于系统并发操作的天性,这个解决方案并不能保证变化通知按照他们产生的顺序依次到达监听器。如果观察者一侧对实际状态的准确性有较高要求,可以考虑把 StateHolder 作为你事件对象的来源。 如果事件顺序这在你的程序里显得至关重要,有一个办法就是可以考虑用一个线程安全的先入先出(FIFO)结构,连同监听器的快照一起,在 setState 方法的保护块里缓冲你的对象。只要 FIFO 结构不是空的,一个独立的线程就可以从一个不受保护的区域块里触发实际事件(生产者-消费者模式),这样理论上就可以不必冒着死锁的危险还能确保一切按照时间顺序进行。我说理论上,是因为到目前为止我也还没亲自这么试过。。

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

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

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

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

异常处理

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

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

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

总结

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

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

原文发布于微信公众号 - java一日一条(mjx_java)

原文发表时间:2016-03-16

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏wblearn

tomcat程序部署到weblogic遇到的问题:对于字符串空的处理不一样

最近将本地tomcat项目部署到weblogic服务器,某个模块下数据加载不出来,很奇怪,在本地tomcat下此模块下数据都可以完整显示出来,怎么到服务器就不行...

982
来自专栏工科狗和生物喵

【计算机本科补全计划】Java学习笔记(一) 安装配置 (Mac Sublime3) 红黄蓝

正文之前 标题后面为啥要加三个字呢。蹭热度不至于,就想着,让更多人知道么。毕竟我以后也会有当爸的一天~ 要是那些人渣站在悬崖上,旁边没啥人看着,我上去踢一脚是做...

3967
来自专栏求索之路

从零开始仿写一个抖音App——Apt代码生成技术、gradle插件开发与protocol协议

本文首发于简书——何时夕,搬运转载请注明出处,否则将追究版权责任。交流qq群:859640274

2194
来自专栏北京马哥教育

数据工程师常用的 Shell 命令

Linux以其强大的命令行称霸江湖,Shell命令是数据极客的必修兵器。探索性数据分析,在需求和数据都不太明确的环境下,使用各种命令进行一次探索与挖掘。从基础的...

3216
来自专栏王大锤

理解消息转发机制

2103
来自专栏大史住在大前端

大前端的自动化工厂(3)—— babel

babel是ES6+语法的编译器,官方网址:www.babeljs.io,用于将旧版本浏览器无法识别的语法和特性转换成为ES5语法,使代码能够适用更多环境。

2133
来自专栏IT技术精选文摘

从Java视角理解系统结构(三)伪共享

从我的前一篇博文中, 我们知道了CPU缓存及缓存行的概念, 同时用一个例子说明了编写单线程Java代码时应该注意的问题. 下面我们讨论更为复杂, 而且更符合现实...

2157
来自专栏Java后端技术栈

Java虚拟机OOM之虚拟机栈和本地方法栈溢出(4)

(1)如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError 异常; (2)如果虚拟机栈可以动态扩展(当前大部分的 Java ...

953
来自专栏FreeBuf

一个漏洞为何能影响数千万服务器以及66%安卓手机?

安全研究团队Perception Point发现Linux系统内核中存在一个高危级别的本地权限提升0day漏洞,编号为CVE-2016-0728。目前有超过66...

2185
来自专栏枕边书

搭建自己的PHP框架心得(一)

前言 说到写PHP的MVC框架,大家想到的第一个词--“造轮子”,是的,一个还没有深厚功力的程序员,写出的PHP框架肯定不如那些出自大神们之手、经过时间和各种项...

4027

扫码关注云+社区

领取腾讯云代金券