java 事件通知_正确获取Java事件通知
java 事件通知
實現觀察者模式以提供Java事件通知似乎是一件容易的事。 但是,容易陷入一些陷阱。 這是我在各種場合不慎造成的常見錯誤的解釋……
Java事件通知
讓我們從一個簡單的bean StateHolder開始,它封裝了帶有適當訪問器的私有int字段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會發現該異常是由我們使用的HashMap的Iterator引發的。 只是我們在代碼中沒有使用任何迭代器,還是我們? 好吧,我們做到了。 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 ),一個單獨的線程就可以從不受保護的塊中觸發實際的事件通知。 這應確保按時間順序排列,而不會冒死機的危險。 我說應該,因為我從未親自嘗試過此解決方案。
鑒于先前實現的語義,使用諸如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是線程安全的,因此我們不再需要受保護的塊。 但請稍等! 我們不是只是學習使用快照進行廣播,而不是遍歷原始集的隱藏迭代器嗎?
可能有點令人困惑,但是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
java 事件通知
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的java 事件通知_正确获取Java事件通知的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 合肥属于一线城市还是二线城市 合肥是一线
- 下一篇: 吃鸡怎么观战 吃鸡如何进入观战模式