正确获取Java事件通知
實(shí)現(xiàn)觀察者模式以提供Java事件通知似乎是一件容易的事。 但是,容易陷入一些陷阱。 這是我在各種場(chǎng)合不慎造成的常見(jiàn)錯(cuò)誤的解釋……
Java事件通知
讓我們從一個(gè)簡(jiǎn)單的bean StateHolder開始,它封裝了帶有適當(dāng)訪問(wèn)器的私有int字段state :
考慮到我們已經(jīng)決定我們的bean應(yīng)該向注冊(cè)的觀察者廣播state changes的消息。 沒(méi)問(wèn)題! 方便的事件和偵聽器定義很容易創(chuàng)建...
// 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 ); }…接下來(lái)我們需要能夠在StateHolder實(shí)例上注冊(cè)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必須進(jìn)行調(diào)整,以觸發(fā)有關(guān)狀態(tài)更改的實(shí)際通知:
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 );} }答對(duì)了! 這就是全部。 作為專業(yè)人士,我們甚至可能已經(jīng)實(shí)施了此測(cè)試驅(qū)動(dòng)程序,并且對(duì)我們?nèi)娴拇a覆蓋范圍和綠色標(biāo)桿感到滿意。 無(wú)論如何,這不是我們從網(wǎng)絡(luò)教程中學(xué)到的嗎?
壞消息來(lái)了:解決方案有缺陷……
并發(fā)修改
給定上述StateHolder ,即使僅在單線程限制內(nèi)使用,也可以很容易地遇到ConcurrentModificationException 。 但是是誰(shuí)引起的,為什么會(huì)發(fā)生呢?
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會(huì)發(fā)現(xiàn)該異常是由我們使用的HashMap的Iterator引發(fā)的。 只是我們?cè)诖a中沒(méi)有使用任何迭代器,還是我們? 好吧,我們做到了。 broadcast for each構(gòu)造的for each基于Iterable ,因此在編譯時(shí)轉(zhuǎn)換為迭代器循環(huán)。
因此,偵聽器在事件通知期間將自己從StateHolder實(shí)例中刪除可能會(huì)導(dǎo)致ConcurrentModificationException 。 因此,代替研究原始數(shù)據(jù)結(jié)構(gòu),一種解決方案是遍歷偵聽器的快照 。
這樣,偵聽器的刪除將不再干擾廣播機(jī)制(但請(qǐng)注意,通知語(yǔ)義也將稍有更改,因?yàn)樵赽roadcast執(zhí)行時(shí)快照不會(huì)反映這種刪除):
private void broadcast( StateEvent stateEvent ) {Set<StateListener> snapshot = new HashSet<>( listeners );for( StateListener listener : snapshot ) {listener.stateChanged( stateEvent );} }但是,如果要在多線程上下文中使用StateHolder怎么辦?
同步化
為了能夠在多線程環(huán)境中使用StateHolder ,它必須是線程安全的。 這可以很容易地實(shí)現(xiàn)。 向我們類的每個(gè)方法添加同步應(yīng)該可以解決問(wèn)題,對(duì)嗎?
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 ) { [...]現(xiàn)在,通過(guò)其內(nèi)部鎖來(lái)保護(hù)對(duì)StateHolder實(shí)例的讀/寫訪問(wèn)。 這使公共方法具有原子性,并確保了不同線程的正確狀態(tài)可見(jiàn)性。 任務(wù)完成!
不完全是……盡管該實(shí)現(xiàn)是線程安全的,但它冒著使用它死鎖應(yīng)用程序的風(fēng)險(xiǎn)。
考慮以下情況: Thread A更改StateHolder S的狀態(tài)。在通知S的偵聽器期間, Thread B嘗試訪問(wèn)S并被阻塞。 如果B對(duì)即將由S的偵聽器之一通知的對(duì)象持有同步鎖,則我們將陷入死鎖。
這就是為什么我們需要縮小同步范圍以聲明狀態(tài)并在受保護(hù)的段落之外廣播事件:
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 );}} }清單顯示了從以前的片段演變而來(lái)的實(shí)現(xiàn),該實(shí)現(xiàn)使用Set實(shí)例作為內(nèi)部鎖提供了適當(dāng)?shù)?#xff08;但有些過(guò)時(shí)的)同步。 偵聽器通知發(fā)生在受保護(hù)的塊之外,因此避免了循環(huán)等待 。
注意:由于系統(tǒng)具有并發(fā)性,因此該解決方案不能保證更改通知按其發(fā)生的順序到達(dá)偵聽器。 如果需要有關(guān)觀察者端的實(shí)際狀態(tài)值的更多準(zhǔn)確性,請(qǐng)考慮提供StateHolder作為事件對(duì)象的源。
如果事件順序是至關(guān)重要的一個(gè)會(huì)想到一個(gè)線程安全的FIFO結(jié)構(gòu)來(lái)緩沖在的守衛(wèi)塊根據(jù)聽眾快照一起事件setState 。 只要FIFO結(jié)構(gòu)不為空( Producer-Consumer-Pattern ),一個(gè)單獨(dú)的線程就可以從不受保護(hù)的塊中觸發(fā)實(shí)際的事件通知。 這應(yīng)該確保按時(shí)間順序排列,而不會(huì)冒死機(jī)的危險(xiǎn)。 我說(shuō)應(yīng)該,因?yàn)槲覐膩?lái)沒(méi)有嘗試過(guò)這個(gè)解決方案。
鑒于先前實(shí)現(xiàn)的語(yǔ)義,使用諸如CopyOnWriteArraySet和AtomicInteger類的線程安全類來(lái)構(gòu)成我們的類,會(huì)使解決方案的詳細(xì)程度降低:
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是線程安全的,因此我們不再需要受保護(hù)的塊。 但請(qǐng)稍等! 我們不是只是學(xué)習(xí)使用快照進(jìn)行廣播,而不是遍歷原始集的隱藏迭代器嗎?
可能有點(diǎn)令人困惑,但是CopyOnWriteArraySet提供的Iterator已經(jīng)是快照。 CopyOnWriteXXX集合是專為此類用例而發(fā)明的-如果大小較小則非常有效,針對(duì)內(nèi)容很少變化的頻繁迭代進(jìn)行了優(yōu)化。 這意味著我們的代碼是安全的。
在Java 8中,使用Iterable#forEach結(jié)合lambda可以進(jìn)一步簡(jiǎn)化broadcast方法。 該代碼當(dāng)然是安全的,因?yàn)檫€在快照上執(zhí)行了迭代:
private void broadcast( StateEvent stateEvent ) {listeners.forEach( listener -> listener.stateChanged( stateEvent ) ); }異常處理
這篇文章的最后一部分討論了如何處理拋出意外RuntimeException的破碎偵聽器。 盡管我通常嚴(yán)格選擇快速失敗的方法,但是在這種情況下,讓此類異常不予處理可能是不合適的。 特別考慮到該實(shí)現(xiàn)可能在多線程環(huán)境中使用。
中斷的偵聽器以兩種方式損害系統(tǒng)。 首先,它可以防止我們的柏忌后通知那些觀察者。 其次,它可能損害可能沒(méi)有準(zhǔn)備好處理該問(wèn)題的調(diào)用線程。 概括而言,它可能導(dǎo)致多種潛行故障,而最初的原因可能很難追查。
因此,將每個(gè)通知屏蔽在try-catch塊中可能會(huì)很有用:
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...} }結(jié)論
如以上各節(jié)所示,Java事件通知有幾點(diǎn)需要牢記。 確保在事件通知期間遍歷偵聽器集合的快照,將事件通知置于同步塊之外,并在適當(dāng)?shù)那闆r下安全地通知偵聽器。
希望我能夠以一種易于理解的方式解決這些細(xì)微問(wèn)題,并且不會(huì)特別弄亂并發(fā)部分。 如果您發(fā)現(xiàn)一些錯(cuò)誤或需要分享其他智慧,請(qǐng)隨時(shí)使用下面的評(píng)論部分。
翻譯自: https://www.javacodegeeks.com/2015/03/getting-java-event-notification-right.html
總結(jié)
以上是生活随笔為你收集整理的正确获取Java事件通知的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: dnf里的快捷键(dnf快捷键大全)
- 下一篇: Java中不一致的操作会扩大规则