非線程安全集合類(這裏的集合指容器Collection,非Set)的迭代器結合了及時失敗機制,但仍然是不安全的。這種不安全表如今許多方面:java
若是不瞭解其不安全之處就隨意使用,就像給程序埋下了地雷,隨時可能引爆,卻不可預知。
ArrayList是一個經常使用的非線程安全集合,下面以基於ArrayList講解幾種表明狀況。git
及時失敗也叫快速失敗,fast-fail。
「及時失敗」的迭代器並非一種完備的處理機制,而只是「善意地」捕獲併發錯誤,所以只能做爲併發問題的預警指示器。它們採用的實現方式是,將計數器的變化與容器關聯起來:若是在迭代期間計數器被修改,那麼hasNext或next將拋出ConcurrentModificationException。然而,這種檢查是在沒有同步的狀況下進行的,所以可能會看到失效的計數器,而迭代器可能並無意識到已經發生了修改。這是一種設計上的權衡,從而下降併發修改操做的檢測代碼對程序性能帶來的影響。github
然而,及時失敗機制十分簡潔(簡單&清晰),同時對集合的性能影響十分小,因此大部分非線程安全的集合類仍然使用這種機制來進行「善意」的提醒。安全
「一般」是由於及時失敗的「善意」性質,它不少時候會給咱們提醒,但有時候也不會給出提醒,有時候甚至給出某種意義上的錯誤提醒。這一小節針對正常的狀況,這是咱們考察一個機制是否值得「採納並完善」的根本屬性。多線程
構造下列程序:併發
… private Collection users = new ArrayList(); // 因此應使用CopyOnWriteArrayList … users.add(new User("張三",28)); users.add(new User("李四",25)); users.add(new User("王五",31)); … public void run() { Iterator itrUsers = users.iterator(); while(itrUsers.hasNext()){ System.out.println("aaaa"); User user = (User)itrUsers.next(); if(「張三」.equals(user.getName())){ // 在迭代過程當中修改集合 itrUsers.remove(); } else { // 正常輸出 System.out.println(user); } } } …
忽略細節,假設有多個線程在同時執行run方法,操做users集合。這時,「一般」會致使及時失敗。這裏的異常可能從next或remove方法中拋出(固然這裏是從next,由於next先執行):源碼分析
private class Itr implements Iterator<E> { … public boolean hasNext() { return cursor != size; } public E next() { checkForComodification(); int i = cursor; if (i >= size) throw new NoSuchElementException(); Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) throw new ConcurrentModificationException(); cursor = i + 1; return (E) elementData[lastRet = i]; } public void remove() { // 迭代器的remove方法 if (lastRet < 0) throw new IllegalStateException(); checkForComodification(); try { ArrayList.this.remove(lastRet); // 集合的remove方法 cursor = lastRet; lastRet = -1; expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } } … }
實際檢查並拋出異常的是checkForComodification方法:性能
private class Itr implements Iterator<E> { … int expectedModCount = modCount; … final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } … }
modCount是當前集合的版本號
,每次修改(增刪改)集合都會加 1;expectedModCount是當前迭代器的版本號
,在迭代器實例化時初始化爲modCount,只有remove方法正常執行(不拋出異常)才能夠修改這個值,與modCount保持同步。this
所以,若是在線程A正常迭代的過程當中,線程B修改了users集合,modCount就會發生變化,這時,線程B的expectedModCount可以與modCount保持同步,線程A的expectedModCount卻發現本身與modCount再也不同步,從而拋出ConcurrentModificationException異常。線程
扯遠些:
對於線程安全的集合類而言,咱們不但願任何失敗。但對於非線程安全的類,有人認爲「應該在假設線程安全的狀況下使用」,因此及時失敗機制徹底沒有必要;有人認爲「集合類的狀態太多(全部非線程安全域的狀態數量的乘積),併發使用時應該給出錯誤提醒,不然很難排查併發問題」,因此及時失敗機制頗有必要。這個問題見仁見智,我的支持後者觀點。
因此,這種及時失敗的檢查是不完備的。
多線程併發修改集合時,拋出ConcurrentModificationException異常做爲及時失敗的提醒,每每是咱們指望的結果。然而,若是在單線程遍歷迭代器的過程當中修改了集合,也會拋出ConcurrentModificationException異常,看起來發生了及時失敗。這不是咱們指望的結果,是一種及時失敗的誤報。
咱們改用集合的remove方法移除user「張三」:
… public void run() { … if(「張三」.equals(user.getName())){ // 在迭代過程當中修改集合 users.remove(user); // itrUsers.remove(); } else { // 正常輸出 System.out.println(user); } … } …
假設只有一個線程執行run方法,在」張三」被刪除以後,下一次執行next方法時,仍舊會拋出ConcurrentModificationException異常,也就是致使了及時失敗。
這時由於集合的remove方法並無維護集合修改的狀態(如對modCount&expectedModCount組合
的修改和檢查):
public class ArrayList<E> extends AbstractList<E> … public boolean remove(Object o) { // 集合的remove方法 if (o == null) { for (int index = 0; index < size; index++) if (elementData[index] == null) { fastRemove(index); return true; } } else { for (int index = 0; index < size; index++) if (o.equals(elementData[index])) { fastRemove(index); return true; } } return false; } … }
這也讓咱們更容易理解及時失敗的本質——依託於對集合修改狀態的維護。這裏的主要緣由看起來是「集合的remove方法破壞了正常維護的集合修改狀態」,但對於使用者而言,集合在單線程環境下卻拋出了ConcurrentModificationException異常,這是因爲及時失敗機制沒有區分單線程與多線程的狀況,統一給出一樣的提醒(拋出ConcurrentModificationException異常),於是是及時失敗的誤報。
除了誤報,及時失敗之僅限於「善意」(有提醒就是「善意」的,沒有也不是「惡意」的)還體如今其可能「丟失」某些併發修改行爲。在這裏,「丟失」意味着不提醒——某些線程併發修改了當前集合,但沒有拋出ConcurrentModificationException異常,及時失敗機制失效了。
利用hasNext方法提早結束線程,能夠主動避過及時失敗的檢查,從而致使修改行爲的丟失:
private class Itr implements Iterator<E> { … public boolean hasNext() { return cursor != size; // 思考:若是刪除了集合的倒數第二個元素,會發生什麼? } … }
仍是單線程的場景下,假設咱們刪除了集合的倒數第二個元素。這時next方法致使cursor=oldSize-1
,同時remove方法致使newSize=oldSize-1
(oldSize是集合修改以前的size值,newSize集合修改以後的);因此hasNext方法會返回false,讓用戶誤覺得集合迭代已經結束(實際上還有最後一個元素),從而循環終止(在咱們的程序裏用hasNext判斷是否結束),沒法拋出ConcurrentModificationException異常,及時失敗失效了。
推廣到多線程的情景是同樣的,由於size是共享的。
很容易忽略的一點是,上述集合修改狀態的維護自己就是在沒有同步的狀況下進行的,所以可能看到更多(遠比上述要多)失效的集合修改狀態,使迭代器意識不到集合發生了修改,這是一種競態條件(Race Condition)。
假設線程A進入迭代器的remove方法,線程B進入迭代器的next方法,如今線程A執行集合的remove方法:
private class Itr implements Iterator<E> { … public void remove() { … ArrayList.this.remove(lastRet); … } … }
首先,假設沒有其餘線程併發修改,則兩個線程均可以經過checkForComodification()的檢查;而後線程A快速的執行集合的remove方法;待線程A執行完集合的remove方法,因爲線程B以前已經經過了檢查,如今就沒法意識到「users集合在線程A中已經發生了變化」。另外,由於幾乎徹底不存在同步措施,modCount的修改也存在競態條件,其餘狀態也沒法保證是否有效。
上面看到了非線程安全集合類的迭代器是不安全的,但在單線程的環境下,這些集合類在性能、維護難度等方面仍然具備不可替代的優點。那麼該如何在兼具必定程度線程安全的前提下,更好的發揮內建集合類的優點呢?總結起來無非兩點:
參考:
- 傳智播客_張孝祥_Java多線程與併發庫高級應用視頻教程/19_傳智播客_張孝祥_java5同步集合類的應用.avi
本文連接:源碼|從源碼分析非線程安全集合類的不安全迭代器
做者:猴子007
出處:https://monkeysayhi.github.io
本文基於 知識共享署名-相同方式共享 4.0 國際許可協議發佈,歡迎轉載,演繹或用於商業目的,可是必須保留本文的署名及連接。