本篇博客介紹CopyOnWriteArrayList類,讀完本博客你將會了解:
- 什么是COW機制;
- CopyOnWriteArrayList的實現原理;
- CopyOnWriteArrayList的使用場景。
經過之前的博客介紹,我們知道ArrayList是線程不安全的。要實現線程安全的List,我們可以使用Vector,或者使用Collections工具類將List包裝成一個SynchronizedList。其實在Java並發包中還有一個CopyOnWriteArrayList可以實現線程安全的List。
在開始之前先貼一段概念
如果有多個調用者(callers)同時請求相同資源(如內存或磁盤上的數據存儲),他們會共同獲取相同的指針指向相同的資源,直到某個調用者試圖修改資源的內容時,系統才會真正復制一份專用副本(private copy)給該調用者,而其他調用者所見到的最初的資源仍然保持不變。優點是如果調用者沒有修改該資源,就不會有副本(private copy)被建立,因此多個調用者只是讀取操作時可以共享同一份資源。
實現原理
Vector這個類是一個非常古老的類了,在JDK1.0的時候便已經存在,其實現安全的手段非常簡單所有的方法都加上synchronized關鍵字,這樣保證這個實例的方法同一時刻只能有一個線程訪問,所以在高並發場景下性能非常低。
SynchronizedList是java.util.Collections中的一個靜態內部類,其實現安全的手段稍微有一點優化,就是把Vector加在方法上的synchronized關鍵字,移到了方法里面變成了同步塊而不是同步方法從而把鎖的范圍縮小了,另外,SynchronizedList中的方法不全都是同步的,比如獲取迭代器方法listIterator()就不是同步的。下面看下CopyOnWriteArrayList怎么實現線程安全的。
CopyOnWriteArrayList這個類就比較特殊了,對於寫來說是基於重入鎖互斥的,對於讀操作來說是無鎖的。還有一個特殊的地方,這個類的iterator是fail-safe的,也就是說是線程安全List里面的唯一一個不會出現ConcurrentModificationException異常的類。
看下CopyOnWriteArrayList的成員變量:
//重入鎖保寫操作互斥
final transient ReentrantLock lock = new ReentrantLock();
//volatile保證讀可見性
private transient volatile Object[] array;
下面再看下添加元素的代碼邏輯
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();//加鎖
try {
Object[] elements = getArray();//讀取原數組
int len = elements.length;
//構建一個長度為len+1的新數組,然后拷貝舊數據的數據到新數組
Object[] newElements = Arrays.copyOf(elements, len + 1);
//把新加的數據賦值到最后一位
newElements[len] = e;
// 替換舊的數組
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
先獲得鎖,然后拷貝元素組並將新元素加入(添加的元素可以是null),再替換掉原來的數組。我們會發現這種實現方式非常不適合頻繁修改的操作。CopyOnWriteArrayList的刪除和修改的操作的原理也是類似的,這邊就不貼代碼了。
最后看下讀操作
//直接獲取index對應的元素
public E get(int index) {return get(getArray(), index);}
private E get(Object[] a, int index) {return (E) a[index];}
從以上的增刪改查中我們可以發現,增刪改都需要獲得鎖,並且鎖只有一把,而讀操作不需要獲得鎖,支持並發。為什么增刪改中都需要創建一個新的數組,操作完成之后再賦給原來的引用?這是為了保證get的時候都能獲取到元素,如果在增刪改過程直接修改原來的數組,可能會造成執行讀操作獲取不到數據。
遍歷時不用加鎖的原因
常用的方法實現我們已經基本了解了,但還是不知道為啥能夠在容器遍歷的時候對其進行修改而不拋出異常。(其實這是一種fail-safe機制)
// 1. 返回的迭代器是COWIterator
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}
// 2. 迭代器的成員屬性
private final Object[] snapshot;
private int cursor;
// 3. 迭代器的構造方法
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}
// 4. 迭代器的方法...
public E next() {
if (! hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
}
//.... 可以發現的是,迭代器所有的操作都基於snapshot數組,而snapshot是傳遞進來的array數組
到這里,我們應該就可以想明白了!CopyOnWriteArrayList在使用迭代器遍歷的時候,操作的都是原數組!
CopyOnWriteArrayLis的缺點
- 內存占用:如果CopyOnWriteArrayList經常要增刪改里面的數據,經常要執行add()、set()、remove()的話,那是比較耗費內存的。因為我們知道每次add()、set()、remove()這些增刪改操作都要復制一個數組出來。
- 數據一致性:CopyOnWrite容器只能保證數據的最終一致性,不能保證數據的實時一致性。從上面的例子也可以看出來,比如線程A在迭代CopyOnWriteArrayList容器的數據。線程B在線程A迭代的間隙中將CopyOnWriteArrayList部分的數據修改了(已經調用setArray()了)。但是線程A迭代出來的是原有的數據。
使用場景
整體來說CopyOnWriteArrayList是另類的線程安全的實現,但並一定是高效的,適合用在讀取和遍歷多的場景下,並不適合寫並發高的場景,因為數組的拷貝也是非常耗時的,尤其是數據量大的情況下。
總結
稍微總結下:
- CopyOnWriteArrayList基於可重入鎖機制,增刪改操作需要加鎖,讀操作不需要加鎖;
- CopyOnWriteArrayList適合用在讀取和遍歷多的場景下,並不適合寫並發高的場景;
- 基於fail-safe機制,不會拋出CurrentModifyException;
- 另外CopyOnWriteArrayList提供了弱一致性的迭代器,從而保證在獲取迭代器后,其他線程對list的修改是不可見的,迭代器遍歷的數組是一個快照。
其他網友的總結:
CopyOnWrite容器即寫時復制的容器。通俗的理解是當我們往一個容器添加元素的時候,不直接往當前容器添加,而是先將當前容器進行Copy,復制出一個新的容器,然后新的容器里添加元素,添加完元素之后,再將原容器的引用指向新的容器。
CopyOnWriteArrayList中add/remove等寫方法是需要加鎖的,目的是為了避免Copy出N個副本出來,導致並發寫。
但是,CopyOnWriteArrayList中的讀方法是沒有加鎖的。
這樣做的好處是我們可以對CopyOnWrite容器進行並發的讀,當然,這里讀到的數據可能不是最新的。因為寫時復制的思想是通過延時更新的策略來實現數據的最終一致性的,並非強一致性。
所以CopyOnWrite容器是一種讀寫分離的思想,讀和寫不同的容器。而Vector在讀寫的時候使用同一個容器,讀寫互斥,同時只能做一件事兒。