???? 在開發(fā)多線程并發(fā)的程序時,對列表進行遍歷是一個很常見的操作。比如說在觀察者模式中,當某個事件發(fā)生時,就需要通知到對應的觀察者進行事件的處理,這里就需要對觀察者列表進行遍歷,逐一觸發(fā)觀察者進行事件的處理。那么,如何保證并發(fā)中的遍歷操作的原子性呢?大概有下面幾種方式:
1. 首先,最容易想到的肯定是使用JAVA內(nèi)置的同步機制-synchronized,把整個遍歷操作當作一個原子操作。
synchronized(lock) { for(Observer ob:observers) { //trigger event ob.update(event); } }
?
這里使用的鎖對象-lock,應該是和對觀察者列表進行增加刪除操作時使用的鎖是同一個。采用這種方式,如果觀察者列表比較少而且觀察者進行事件的處理時間比較短的時候,是可接受的。如果觀察者列表變的很大或者其中某幾個觀察者進行事件的處理占時比較長的時候,那么就會引發(fā)并發(fā)的liveness問題,從而引起性能的下降。
?
2.索引訪問
??針對第一種遍歷出現(xiàn)的問題,我們可以通過減少synchronized的范圍來進行優(yōu)化。每當我們看到synchronized塊的時候,我們要問問自己,synchronized塊保護的是什么?很明顯,上面保護的是observers列表,保證對該列表并發(fā)訪問時數(shù)據(jù)的正確性。也就是說,對于ob.update(event),根本不需要進行保護。從而出現(xiàn)了Doug Lea所說的索引訪問:
Observer ob=null; for(int i=0;true;i++) { synchronized(observers) { if(i<observers.size()) ob=observers.get(i); else break; } //下面update調(diào)用不需要同步 ob.update(event); }
??? 雖然這種方式對性能有所提升,但是有可能會出現(xiàn)這樣的問題,如果在遍歷過程中,對observers列表進行刪除,則有可能出現(xiàn)被刪除的Observer沒有機會進行事件的觸發(fā),或者對observers列表進行增加一個相同的元素,而observers本身就允許增加相同元素的時候,那么同一個Observer可能會對同一事件進行兩次處理。而對于第一種遍歷方式的另外一種改進,則是通過在持有鎖的情況下,復制一份observers列表,然后在不持有鎖的情況下,對observers列表中的每個observer進行事件的通知:
Object[] tempObservers=null; synchronized(observers) { tempObservers=Arrays.copyOf(observers,observers.length); } for(Observer ob:tempObservers) ob.update(event);
?
?
3.Fail-fast
?? 如果不允許第2種遍歷方式出現(xiàn)的問題,那么可以通過使用Fail-fast模式來進行強制避免。Fail-fast模式經(jīng)常出現(xiàn)在java的Iterator中。在Fail-fast模式中,每個列表維護一個int型的版本變量,每次對這個列表進行更新操作都會使這個版本變量加1。
public class List { int version; public synchronized void add(Object obj) { //add object array[cursor++]=obj; version++; } public synchronized void remove(int index) { //remove array[index]=null; version++; } //其余略 }
?? 而在創(chuàng)建一個Iterator時,會將當前的版本變量保存,在通過Iterator進行遍歷的時候,會每次都將Iterator內(nèi)部保存的版本變量和List列表維護的版本變量進行比較,如果不相等,則表示有線程對當前的List列表進行了更新操作,那么Iterator的遍歷方式會拋出一個異常進行中止遍歷。
public class List { int version; .... public Iterator iterator() { return new ListIterator(); } //inner class class ListIterator implements Iterator { int iteratorVersion; public ListIterator() { iteratorVersion=version; } public boolean hasNext() { synchronized(List.this){ if(iteratorVersion!=version) throw new RuntimeException(); //check if the next element exists } } public Object next() { synchronized(List.this){ if(iteratorVersion!=version) throw new RuntimeException(); //return the next element } } }
? ? 如果你看過JDK的Iterator實現(xiàn),你就會發(fā)現(xiàn)JDK中提供的大多數(shù)Iterator都不是同步的,即使是使用Collections.synchronizedList()后的list,這也就導致了我們常常在使用JDK提供的Iterator時,要自己考慮遍歷時的同步,否則會出現(xiàn)這樣的問題,iterator的遍歷是在一個線程,而對集合進行更新操作的是在另一個線程,那么,由于iterator遍歷的過程沒有同步,而version變量也不是violate,這樣就沒法保證version變量在并發(fā)中的可見性從而引起問題。而上述的實現(xiàn)了一個有鎖的版本。
???? 其實,上面這個synchronized版本的iterator可以進一步優(yōu)化,可以將version變量聲明為violate,從而將所有的的鎖給去掉。雖然Fail-fast模式的遍歷可以保證并發(fā)遍歷操作時數(shù)據(jù)的正確性,但是這種遍歷方式很不友好,特別是在大并發(fā)的情況下,F(xiàn)ail-fast遍歷拋出異常中止的機率很高,試想下,如果在觀察者中使用Fail-fast的遍歷方式來通知事件的發(fā)生,那么在大并發(fā)的情況下,一件事件的發(fā)生并不保證會通知到所有的觀察者,這樣有可能就造成了事件的丟失,這個問題導致了Fail-fast的遍歷方式的使用業(yè)務場景會比較窄。
??
4.copy on write
???而對于copy on write方式,其原理是,每次在對集合進行更新操作的時候,都會對底層的數(shù)組進行拷貝。比如說add,remove操作。
public class CopyOnWriteList { public synchronized void add(Object obj) { Object[] newArray=Arrays.copyOf(oldArray,oldArray.length+1); newArray[oldArray.length]=obj; oldArray=newArray; } ..... }
??? 那么就可以在進行遍歷操作的時候,不需要加鎖來保證同步。
??
public class CopyOnWriteList { Object[] oldArray; .... public Iterator iterator() { return new CopyOnWriteIterator(oldArray); } public CopyOnWriteIterator implements Iterator { private Object[] iteratorArray; private int cursor; public CopyOnWriteIterator(Object[] array) { iteratorArray=array; } //不需要加鎖 public boolean hasNext() { return cursor++<iteratorArray.length; } //不需要加鎖 public Object next() { return iteratorArray[cursor]; } }
???雖然說copy on write的方式避免了遍歷時加鎖的問題,但是這種方式只適用于對于集合更新操作比較少,但遍歷使用場景比較多的情況下,?否則頻繁的COPY操作也會使性能下降。這種方式現(xiàn)在廣泛的應用于觀察者模式中,因為觀察者模式更多的是進行一個事件的通知(即遍歷集合),而不是增加/刪除觀察者。
?
5.鏈表-通過對add,remove等更新操作分別進行特殊處理,使得遍歷不加鎖。
?? 這種實現(xiàn)方式?jīng)]有鎖,也沒有violate變量,而是通過維護一個單向鏈表,并分別對Add,Remove操作進行一些額外的限制。對于Add操作來說,新加入的節(jié)點必須加到原先鏈表頭結點之前,新加入的節(jié)點變成新的鏈表頭節(jié)點:
?????通過這樣的方式,在遍歷過程中進行節(jié)點的增加操作也不會有并發(fā)問題,而對應的remove操作則需要復制被刪除節(jié)點之前的所有節(jié)點,并與被刪除的節(jié)點后面的節(jié)點關聯(lián)起來,如果要刪除的節(jié)點是最后一個節(jié)點,則相當于復制了整個鏈表。
?????????
?
??????? 如果在遍歷過程中進行刪除,也不會有并發(fā)問題,原先的遍歷會一直執(zhí)行,新的遍歷會從新復制的節(jié)點開始。因為這里的刪除操作是對原來的節(jié)點進行了復制,而原先的節(jié)點在遍歷結束后被GC回收掉。
更多文章、技術交流、商務合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯(lián)系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
