前言
之前看《Java併發程式設計》這本書的時候,有看到這個,只記得"讀多寫少"、"寫入時複製"。書中沒有過多講述,只是一筆帶過(不過現在回頭看,發現講的都是精髓。老外的書大多重理論,喜歡花大篇幅講概念,這點我非常喜歡)記得當時是覺得可能有點難,先跳過了,結果就忘記回頭看了。今天突然想起來,就看了一下,整理一點東西。
非執行緒安全的ArrayList
我們知道原來util包中的ArrayList是不提供同步的,也就是說當多個執行緒讀寫ArrayList的時候可能出現執行緒安全問題。例如,就add操作而言
ArrayList的add方法:
我們把elementData[size++] = e 分為兩步:
- elementData[size]=e
- size++
如果呼叫兩次add操作,期待結果應該是這樣的:
但是,如果存在兩個執行緒A、B幾乎同時操作add方法,由於無法保證add操作的原子性,實際操作時序可能如下。
那麼,對應的結果就會是這樣:
這裡就發生大問題了,e1被後續新增的e2覆蓋。e1丟失,而size卻仍舊遞增兩位。
執行緒安全的ArrayList——SynchronizedList
Collections實用類(注意,不是Collection介面)提供同步容器包裝,將普通的集合包裝成執行緒安全的集合。
例如,通過Collections.synchronizedList(List<T>)方法,可以把一個非執行緒安全的List集合變為執行緒安全的集合。
這裡其實是一個裝飾器模式的應用,引數集合List將被裝飾為SynchronizedList。
其是Collections的內部類。
通過對每個方法呼叫都進行同步加鎖,使得多個執行緒讀寫ArrayList只能按序進行。這樣的話,資料的安全性和一致性都得到了保證。
缺點也非常明顯,每個執行緒讀寫ArrayList都需進行同步,開銷大。
迭代過程中的異常 —— ConcurrentModificationException
在ArrayList的實現中,其迭代器實現了一個方法checkForComodification 這個方法會檢查迭代期間是否有其他執行緒修改了集合,如果有,則丟擲ConcurrentModificationException。
原理
主要跟兩個欄位有關:
- expectedModCount(來自ArrayList的迭代器Itr)
- modCount(來自ArrayList的父類AbstractList,初始為0)。
ArrayList實現中,每執行一次新增操作,都會讓modCount+1。
注意:addAll也是讓modCount+1,與新增的元素個數無關。remove和set操作不算,其不會讓modCount有所改變。
ArrayList的迭代器中,expectedModCount的初始值被設定為modCount。
迭代器在每次遍歷時,會呼叫checkForComodification 檢查狀態,如果此過程中集合發生了改動,則直接丟擲異常。
注:如果迭代期間需要修改集合,只能通過迭代器的方法修改集合,這些方法不會觸發異常。因為其會重置expectedModCount的值為當前modCount。
如何防止迭代過程出現異常?
所以,在使用這樣的ArrayList時,如果需要對其進行迭代,則需要對容器進行加鎖(或者拷貝一份),使當前執行緒對其獨佔訪問,以保證其迭代過程能夠正常執行。如下:
public class SomeClass { List<E> list; public SomeClass(List<E> list) { this.list = list; } //如果這個方法會被多執行緒訪問,那麼最好對list的訪問進行加鎖 public void function() { synchronized(list) { for(E e:list) { .... } } } }
執行緒安全的另一種實現類——CopyOnWriteArrayList
CopyOnWriteArrayList同樣是執行緒安全的ArrayList,但是與SynchronizedList不同的是,它只對寫操作加鎖,對讀操作不加鎖。關鍵是,其在迭代期間不需要對容器進行加鎖或複製。這一切都與"寫入時複製"有關。
寫入時複製的原理
CopyOnWriteArrayList的欄位:
- lock => 鎖,寫操作時需要
- array => 容器陣列的引用。指向儲存當前元素的陣列。
與容器陣列引用直接相關的兩個方法:
寫入時複製相關程式碼:
public boolean add(E e) { final ReentrantLock lock = this.lock; //寫操作還是要上鎖的,此鎖是全域性鎖 lock.lock(); try { //獲取容器陣列 Object[] elements = getArray(); //獲得容器長度 int len = elements.length; //建立一個新的儲存空間,容量+1 Object[] newElements = Arrays.copyOf(elements, len + 1); //在新的儲存空間內,加入新元素 newElements[len] = e; //修改當前容器陣列的引用 setArray(newElements); return true; } finally { //釋放鎖 lock.unlock(); } }
當add操作完成後,array的引用就已經指向另一個儲存空間了。 這裡也暴露了一個缺點:如果此容器的寫操作比較頻繁,那麼其開銷就比較大。
迭代器實現
CopyOnWriteArray有自己的迭代器,該迭代器不會檢查修改狀態,也無需檢查狀態。因為迭代的陣列是可以說是隻讀的,不會有其他執行緒能夠修改它。
迭代器,引用的陣列變數名就叫snapshot(快照)。也從另一個角度說明,在迭代器迭代過程中,其使用的是容器的過去一個版本,一個快照。不能保證是當前容器的狀態。
這裡也暴露了一個缺點:不能保證資料的瞬時一致性。
但是,其有一個顯著的優點,那就是讀操作,和遍歷操作不需要同步。多執行緒訪問的時候,速度較高。
CopyOnWriteArrayList應用場景
由以上的優缺點可得,CopyOnWriteArrayList應用的場景,最好是讀操作多,寫操作相對較少的場景("讀多寫少")。也就是說,集合內容不會經常變動的。例如,網上常說的"黑名單"這類東西。