阅读更多

5顶
0踩

编程语言

原创新闻 正确使用Java事件通知

2015-03-31 14:04 by 副主编 mengyidan1988 评论(1) 有7905人浏览
通过实现观察者模式来提供 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<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 );
  }
}

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

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

并发修改

像上面那样写 StateHolder 很容易遇到并发修改异常(ConcurrentModificationException),即使仅仅限制在一个单线程里面用也不例外。但究竟是谁导致了这个异常,它又为什么会发生呢?
java.util.ConcurrentModificationException
    at java.util.HashMap$Hash Iterator.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<StateListener> 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<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 结构不是空的,一个独立的线程就可以从一个不受保护的区域块里触发实际事件(生产者-消费者模式),这样理论上就可以不必冒着死锁的危险还能确保一切按照时间顺序进行。我说理论上,是因为到目前为止我也还没亲自这么试过。。

鉴于前面已经实现的,我们可以用诸如 CopyOnWriteArraySet 和 AtomicInteger 来写我们的这个线程安全类,从而使这个解决方案不至于那么复杂:
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 );
    }
  }
}

既然 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的事件通知里面有一些基本要点你还是必须得记住的。在事件通知过程中,要确保在监听器集合的快照里做迭代,保证事件通知在同步块之外,并且在合适的时候再安全地通知监听器。

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

本文由 ImportNew - 林申 翻译自 javacodegeeks
5
0
评论 共 1 条 请登录后发表评论
1 楼 chokali 2015-04-01 10:29

发表评论

您还没有登录,请您登录后再发表评论

相关推荐

  • 正确用法Java大事通知_.docx

    正确用法Java大事通知_.docx

  • java实现短信通知、验证码

    目的是用Java实现短信单发功能,可以是验证码或者短信通知,使用阿里云短信服务,总体来说比较简单,注册账号后申请签名和模板等等拿到一些参数,然后调用发送短信的方法就行。

  • java zookeeper 使用_java 中 zookeeper简单使用

    } } } 然后,执行可以看到,控制台输出如下: 所以,像一些公用的配置,我们可以存到zookeeper里面,之后其它的服务就可以使用了 总结 以上所述是小编给大家介绍的java 中 zookeeper简单使用,希望对大家有所帮助,...

  • 10万字208道Java经典面试题总结(附答案)

    JDK(Java Development Kit),Java开发工具包 JRE(Java Runtime Environment),Java运行环境 JDK中包含JRE,JDK中有一个名为jre的目录,里面包含两个文件夹bin和lib,bin就是JVM,lib就是JVM工作所需要的类库。...

  • Java对接钉钉事件订阅(审批事件)

    项目中刚好涉及到了需要对接钉钉事件订阅(审批事件),谨以此篇博客和大家分享下。 需求 需要把钉钉每次发起请假流程审批的数据存入数据库。 总体流程 钉钉发起请假申请 -&gt; 钉钉服务器把消息推送给我们项目...

  • Java支付宝APP支付-验证异步通知消息

    上一章已经讲述了支付宝如何生成支付订单,这一章讲述一下支付宝生成订单之后,异步通知接口的开发。 这里先讲一下啥叫支付宝异步通知:对于App支付产生的交易,支付宝会根据原始支付API中传入的异步通知地址notify...

  • 掌握P5级Java面试技巧

    HashMap底层原理,扩容机制,jdk8以后会使用红黑树优化?红黑树和二叉平衡树的区别,红黑树和B树,B+树的区别,Mysql二大引擎索引底层实现,HashMap在多线程环境中为何出错?ConcurrentHashMap底层实现,CAS,原子...

  • Java微信APP支付-退款结果通知

    前面已经讲过微信APP支付的统一下单、支付结果通知、申请退款的接口开发,现在我们讲述一下退款结果通知的流程开发。 官方的API地址:https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=9_16&amp;amp;...

  • java中spring的使用

    java中spring的使用  Spring是一个开源框架,框架的主要优势之一就是其分层架构,分层架构允许使用者选择使用哪一个组件,同时为 J2EE 应用程序开发提供集成的框架。Spring使用基本的JavaBean来完成以前只可能由...

  • Java微信APP支付-支付结果通知

    上一章讲了微信APP支付统一下单接口的开发,这一章我们讲支付结果通知接口的开发,这一接口是微信异步调用我们的接口,告之我们支付已经成功了,然后我们补录门店、电商订单,更新APP订单支付信息等业务逻辑。...

  • Java集成Redis key过期通知

    为什么要使用过期通知呢? 捕获Redis中过期的Key,解锁新姿势。比如有个用户会员的模块,那么可以在redis添加一个用户会员的有效时Key,然后在Java项目中捕获,处理相关的逻辑。 一、开启Redis Key过期通知的配置 这...

  • java:CompletableFuture使用

    CompletableFuture针对Future的一些缺点进行了优化,例如回调通知,异步任务完成或产生异常,自动调用回调方法;创建异步任务,多个任务前后可组合处理等等。2 使用2.1 CompletableFuture的4个静态方法简介...

  • Java面试题大全(2021版)

    编程就是让计算机为解决某个问题而使用某种程序设计语言编写程序代码,并最终得到结果的过程。 为了使计算机能够理解人的意图,人类就必须要将需解决的问题的思路、方法、和手段通过计算机能够理解的形式告诉计算机...

  • 微信学习(一)——java代码发送服务通知

    * @Description: 微信小程序发送模板消息(服务通知)的入参 */ public class WxMessageDTO { private String touser; // 用户的openid private String template_id; // 所需下发的模板消息的id private String ...

  • Java基础知识面试题(2020最新版)

    文章目录Java概述何为编程什么是Javajdk1.5之后的三大版本JVM、JRE和JDK的关系什么是跨平台性?原理是什么Java语言有哪些特点什么是字节码?采用字节码的最大好处是什么什么是Java程序的主类?应用程序和小程序的...

  • Java中23种设计模式

    Java中23种设计模式,包括简单介绍,适用场景以及优缺点等

  • java plugin firefox_Firefox 中使用 Java 插件

    本文转自Firefox官方网站许多网页使用 Java 小程序 用于交互式内容,如在线游戏。Java 是一种编程语言,它可以运行在 Windows、Mac、Linux 及其他计算机系统。在 Firefox 运行 Java 小程序之前,您必须已经正确安装...

  • WebAudioAPIError(解决方案).md

    项目中常见的问题,记录一下解决方案

Global site tag (gtag.js) - Google Analytics