正确获取Java事件通知

实现观察者模式以提供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;}
}

考虑到我们已经决定我们的bean应该向注册的观察者广播state changes的消息。 没问题! 方便的事件和侦听器定义很容易创建...

// 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<StateListener> 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 );}
}

答对了! 这就是全部。 作为专业人士,我们甚至可能已经实施了此测试驱动程序,并且对我们全面的代码覆盖范围和绿色标杆感到满意。 无论如何,这不是我们从网络教程中学到的吗?

坏消息来了:解决方案有缺陷……

并发修改

给定上述StateHolder ,即使仅在单线程限制内使用,也可以很容易地遇到ConcurrentModificationException 。 但是是谁引起的,为什么会发生呢?

java.util.ConcurrentModificationExceptionat 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)

查看stacktrace会发现该异常是由我们使用的HashMapIterator引发的。 只是我们在代码中没有使用任何迭代器,还是我们? 好吧,我们做到了。 broadcast for each构造的for each基于Iterable ,因此在编译时转换为迭代器循环。

因此,侦听器在事件通知期间将自己从StateHolder实例中删除可能会导致ConcurrentModificationException 。 因此,代替研究原始数据结构,一种解决方案是遍历侦听器的快照

这样,侦听器的删除将不再干扰广播机制(但请注意,通知语义也将稍有更改,因为在broadcast执行时快照不会反映这种删除):

private void broadcast( StateEvent stateEvent ) {Set<StateListener> snapshot = new HashSet<>( listeners );for( StateListener listener : snapshot ) {listener.stateChanged( stateEvent );}
}

但是,如果要在多线程上下文中使用StateHolder怎么办?

同步化

为了能够在多线程环境中使用StateHolder ,它必须是线程安全的。 这可以很容易地实现。 向我们类的每个方法添加同步应该可以解决问题,对吗?

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实例的读/写访问。 这使公共方法具有原子性,并确保了不同线程的正确状态可见性。 任务完成!

不完全是……尽管该实现线程安全的,但它冒着使用它死锁应用程序的风险。

考虑以下情况: Thread A更改StateHolder S的状态。在通知S的侦听器期间, Thread B尝试访问S并被阻塞。 如果B对即将由S的侦听器之一通知的对象持有同步锁,则我们将陷入死锁。

这就是为什么我们需要缩小同步范围以声明状态并在受保护的段落之外广播事件:

public class StateHolder {private final Set<StateListener> 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<StateListener> snapshot;synchronized( listeners ) {snapshot = new HashSet<>( listeners );}for( StateListener listener : snapshot ) {listener.stateChanged( stateEvent );}}
}

清单显示了从以前的片段演变而来的实现,该实现使用Set实例作为内部锁提供了适当的(但有些过时的)同步。 侦听器通知发生在受保护的块之外,因此避免了循环等待

注意:由于系统具有并发性,因此该解决方案不能保证更改通知按其发生的顺序到达侦听器。 如果需要有关观察者端的实际状态值的更多准确性,请考虑提供StateHolder作为事件对象的源。

如果事件顺序至关重要的一个会想到一个线程安全的FIFO结构来缓冲在的守卫块根据听众快照一起事件setState 。 只要FIFO结构不为空( Producer-Consumer-Pattern ),一个单独的线程就可以从不受保护的块中触发实际的事件通知。 这应该确保按时间顺序排列,而不会冒死机的危险。 我说应该,因为我从来没有尝试过这个解决方案。

鉴于先前实现的语义,使用诸如CopyOnWriteArraySetAtomicInteger类的线程安全类来构成我们的类,会使解决方案的详细程度降低:

public class StateHolder {private final Set<StateListener> 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 );}}
}

由于CopyOnWriteArraySetAtomicInteger是线程安全的,因此我们不再需要受保护的块。 但请稍等! 我们不是只是学习使用快照进行广播,而不是遍历原始集的隐藏迭代器吗?

可能有点令人困惑,但是CopyOnWriteArraySet提供的Iterator已经是快照。 CopyOnWriteXXX集合是专为此类用例而发明的-如果大小较小则非常有效,针对内容很少变化的频繁迭代进行了优化。 这意味着我们的代码是安全的。

在Java 8中,使用Iterable#forEach结合lambda可以进一步简化broadcast方法。 该代码当然是安全的,因为还在快照上执行了迭代:

private void broadcast( StateEvent stateEvent ) {listeners.forEach( listener -> listener.stateChanged( stateEvent ) );
}

异常处理

这篇文章的最后一部分讨论了如何处理抛出意外RuntimeException的破碎侦听器。 尽管我通常严格选择快速失败的方法,但是在这种情况下,让此类异常不予处理可能是不合适的。 特别考虑到该实现可能在多线程环境中使用。

中断的侦听器以两种方式损害系统。 首先,它可以防止我们的柏忌通知那些观察者。 其次,它可能损害可能没有准备好处理该问题的调用线程。 概括而言,它可能导致多种潜行故障,而最初的原因可能很难追查。

因此,将每个通知屏蔽在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事件通知有几点需要牢记。 确保在事件通知期间遍历侦听器集合的快照,将事件通知置于同步块之外,并在适当的情况下安全地通知侦听器。

希望我能够以一种易于理解的方式解决这些细微问题,并且不会特别弄乱并发部分。 如果您发现一些错误或需要分享其他智慧,请随时使用下面的评论部分。

翻译自: https://www.javacodegeeks.com/2015/03/getting-java-event-notification-right.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/360878.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

使用fn函数控制页面显示内容

在使用&#xff25;&#xff2c;的时候&#xff0c;不可避免的遇到&#xff0c;截取字符串&#xff0c;判断字符串长度等情况。这里给出简单的通过&#xff46;&#xff4e;函数操作字符串的deamon。 1、页面引入标签 <% taglib prefix"c" uri"http://java.s…

uni-app微信获取手机号,第一次解密总是失败

项目场景&#xff1a; uni-app; 获取code&#xff0c;后台解密手机号 问题描述&#xff1a; 每次第一次登陆&#xff0c;后台都会解密失败 原因分析&#xff1a; code获取错误&#xff1b;导致后台的解密key与code不对应 解决方案&#xff1a; 小程序获取手机号之前&#xf…

Java中不一致的操作会扩大规则

总览 当您在Java中执行一元或二进制操作时&#xff0c;标准行为是使用最宽的操作数&#xff08;或对byte &#xff0c; short和char使用更宽的操作数&#xff09;。 这很容易理解&#xff0c;但是如果考虑最佳类型可能会造成混淆。 乘法 当执行乘法运算时&#xff0c;您得到的…

圣诞节到了,用js给喜欢的人写一颗圣诞树吧

文章目录 1、效果预览2、代码2.1、定义数组写下祝福语2.2、模拟雪花落下的效果2.3、设置背景粒子2.4、操作动画效果2.5、定义闪烁效果2.6、定义粒子对象2.7、粒子对象播放2.8、绘制星星2.9、绘制圣诞树2.10、绘制星星背景动画2.11、定义初始化函数并调用 3、结尾 1、效果预览 圣…

Unity3D 访问Access数据库

Unity3D 访问Access数据库 在开始这个小教程之前呢&#xff0c;其实在网上你已经可以找到相关的资料了&#xff0c;但是我还是要把我自己做练习的一点东西分享出来。写这个教程的主要原因呢&#xff0c;是一个朋友在u3d的官网论坛里&#xff0c;找到了这个demo&#xff0c;但是…

uni.reLaunch前出现uni.showToast,不会成功弹出提示信息

解决方案&#xff1a; uni.showToast({title: 发布成功,duration: 1000});setTimeout(function() {uni.reLaunch({url: /pages/tips/index})}, 1000);

LaTeX 基础笔记。开篇

LaTeX 的起源非常牛逼&#xff0c;有一套书大家可能听说过《计算机程序设计艺术》&#xff0c;写了好几本。当然能在计算机方面写上艺术俩字的书恐怕不是我们一般人能读懂得东西了。他的作者在1976年准备写第二卷的时候发现计算机的排版非常难看&#xff0c;所以&#xff0c;为…

Java旧版不断发展

我最近偶然发现了JDK API的一个非常有趣的警告&#xff0c;即Class.getConstructors()方法。 它的方法签名是这样的&#xff1a; Constructor<?>[] getConstructors()有趣的是&#xff0c; Class.getConstructor(Class...)返回一个Constructor<T> &#xff0c;并…

React 学习笔记 —— Ref Hook

用以下三种方式创建 Ref 都可以 import React from reactexport default function Count () {const [count ,setCount] React.useState(0)const myRef React.createRef()const myRef2 React.useRef() // Ref Hook 的方式const myRef3 {current: undefined}const addNumber…

MFC消息机制

MFC消息机制 MFC消息机制涉及许多知识&#xff0c;比如消息分类&#xff0c;消息映射等。知识先了解一下&#xff0c;马上动手实践才是硬道理。我建了个SDI项目&#xff0c;把常用的消息试验了一遍。如果像我一样初学的&#xff0c;可以留下邮箱索取源码。// MainFrm.h afx_msg…

带Lambda表达式的Apache Wicket

这是怎么回事&#xff1f; :) 我一直在从事一些项目&#xff0c;这些项目值得庆幸的是将Apache Wicket用于表示层。 我自然想到Java的8个lambda表达式如何与Wicket完美匹配。 而不仅仅是我&#xff0c; Wicket团队似乎已经在努力更改API&#xff0c;以为开箱即用的lambda提供支…

React 父组件和子组件中的方法相互调用

目录父组件调用子组件方法子组件调用父组件方法父组件调用子组件方法 父组件中调用子组件的getTree方法 父组件 setFormValue()>{this.TreeList.getTree}<TreeList onSelect{this.setFormValue} onRef{(ref) > { this.TreeList ref }} />子组件 componentDidMount…

元素在父元素内垂直居中的思路

1.使用表格 的垂直居中特性 2.div的绝对定位 已知高度的情况下比较好弄. 3.用背景实现.前景元素visibility:hidden; 4.父元素table-cell 5.line-height 图片会跟随文字垂直居中.转载于:https://www.cnblogs.com/fumj/archive/2013/03/27/2984623.html

装饰者模式如何拯救了我的一天

在工作中&#xff0c;我正在处理庞大的Java代码库&#xff0c;该代码库是由许多不同的开发人员在15年的时间里开发的。 并不是所有的事情都由书来完成&#xff0c;但是同时我通常没有机会重构遇到的每一个奇怪之处。 尽管如此&#xff0c;仍可以每天采取提高代码质量的措施。 …

【虚拟主机篇】asp页面实现301重定向方法

301重定向在很多地方都需要用到&#xff0c;也是seo中常见的问题。比如确定首选域或更换网站域名的时候都要用到301重定向。301重定向的方法有好几种&#xff0c;拿ASP类网站来说有&#xff1a;首页301重定向和全站301重定向。 首页301重定向的方法&#xff1a; <% website…

快速的骆驼和云消息传递

Apache Camel是一个流行的&#xff0c;成熟的开源集成库。 它实现了企业集成模式 &#xff0c;这是在集成分布式系统时经常出现的一组模式。 过去&#xff0c;我写过很多关于Camel的文章&#xff0c; 包括为什么我比Spring Integration更喜欢它 &#xff0c; 路由引擎 如何 工作…

antd react dva在model中使用另一个model的state值

const oldData yield select(({ baseDictionary }) > {return ([...customPageSetting.list,]) });

三角形类1

/* 程序的版权和版本声明部分 Copyright (c)2012, 烟台大学计算机学院学生 All rightsreserved. 文件名称&#xff1a; object.cpp 作者&#xff1a;刘清远 完成日期&#xff1a; 2013年3月29日 版本号&#xff1a; v1.0 输入描述&#xff1a;无 问题描述&#xff1a;设计求三…

不可将您的方法命名为“等于”

&#xff08;当然&#xff0c;除非您确实重写了Object.equals() &#xff09;。 我偶然发现了用户Frank的一个非常奇怪的Stack Overflow问题 &#xff1a; 为什么Java的Area&#xff03;equals方法不能覆盖Object&#xff03;equals&#xff1f; 有趣的是&#xff0c;有一个A…

C#GRPC 服务端与客户端通信,故障排除记录

文章目录前言一、问题一解决方法二、问题二解决方法前言 第一次建立GRPC服务端&#xff0c;客服端一直通不到服务端&#xff1b; 问题1&#xff1a; One or more errors occurred. (Status(StatusCodeInternal, Detail"Error starting gRPC call. HttpRequestException:…