概述
Java自1.5後提供了兩個寫時複製的容器,分別是CopyOnWriteArrayList和CopyOnWriteArraySet。其思路就是在執行會改變底層資料的結構時,首先加鎖,然後複製得到一個新的資料,在這個資料上做修改,最後再將原來的資料引用指向這個新的資料,最後釋放鎖;而讀操作則不需要修改。這是一種讀寫分離的思想,讀和寫不同的容器,讀的是舊容器,寫的是新容器。
由於CopyOnWriteArrayList的實現原理和CopyOnWriteArraySet類似,所以就以CopyOnWriteArrayList拋個磚。
下圖是CopyOnWriteArrayList的類繼承關係,如下圖:
可以發現實現了List介面,直接繼承了Object類,而沒有像很多List一樣繼承AbstractList類。CopyOnWriteArrayList是一個自動擴容的List,允許任何元素,包括null。 源碼分析 重要欄位
CopyOnWriteArrayList名中含有“ArrayList”,所以其內部是基於數組實現的一個列表。為了使讀寫分離,所以不能在類自身上鎖,內部含有一把鎖,欄位如下:
/** The lock protecting all mutators */ final transient ReentrantLock lock = new ReentrantLock(); /** The array, accessed only via getArray/setArray. */ private transient volatile Object[] array;
add(E e)方法
下面看一下添加一個元素的方法:
public boolean add(E e) { final ReentrantLock lock = this.lock; //加鎖 lock.lock(); try { //得到舊資料 Object[] elements = getArray(); int len = elements.length; //建立新資料 Object[] newElements = Arrays.copyOf(elements, len + 1); //操作新資料 newElements[len] = e; //更改舊資料 setArray(newElements); return true; } finally { //解鎖 lock.unlock(); } }
從上面的代碼可以看到添加一個元素add方法的執行流程:
1. 對Lock加鎖
2. 一旦得到鎖後,根據舊資料建立新資料
3. 更改新資料
4. 將引用指向新資料
上面代碼中的setArray()方法就是將array引用指向新建立的數組,該方法的實現如下:
final void setArray(Object[] a) { array = a; }remove(int index)方法
下面再看一下rmeove(int index)方法,如果index超出了索引,那麼會拋出IndexOutBoundsException,實現如下:
public E remove(int index) { final ReentrantLock lock = this.lock; //加鎖 lock.lock(); try { //得到舊資料 Object[] elements = getArray(); int len = elements.length; //獲得元素,該方法可能會拋出IndexOutBoundsException E oldValue = get(elements, index); //計算左移資料個數 int numMoved = len - index - 1; //如果不需要左移,更新引用指向 if (numMoved == 0) setArray(Arrays.copyOf(elements, len - 1)); //需要左移 else { //建立新資料 Object[] newElements = new Object[len - 1]; //複製左半邊 System.arraycopy(elements, 0, newElements, 0, index); //複製右半邊 System.arraycopy(elements, index + 1, newElements, index, numMoved); //更新引用 setArray(newElements); } return oldValue; } finally { //釋放鎖 lock.unlock(); } }
從上面的代碼可以看到,remove()方法的執行流程和add()相同,都是:
1. 加鎖
2. 複製舊資料,修改新資料
3. 將引用指向新資料
4. 釋放鎖 寫方法總結
CopyOnWriteArrayList中的add、set、remove等方法會修改底層數組,所以當執行這些方法時會首先擷取Lock鎖,也就意味著同一時刻只能有一個線程在修改資料。 get(int index)方法
下面看一下get()方法的實現:
public E get(int index) { return get(getArray(), index); }private E get(Object[] a, int index) { return (E) a[index]; }final Object[] getArray() { return array; }
可以看到該方法沒有加鎖。 size()方法
下面是size()方法的實現:
public int size() { return getArray().length; }讀方法總結
CopyOnWriteArrayList中的get、size、isEmpty等方法不會修改底層資料,所以當執行這些方法時不會擷取Lock鎖,直接對舊資料進行讀取。 拓展
Java中提供了CopyOnWriteArrayList和CopyOnWriteArraySet,而沒有提供CopyOnWriteMap,但是經過上面的源碼分析,我們已經知道了COW的原理,那就是寫時加鎖,讀時不加鎖。按照這種思想實現了一個CopyOnWriteMap,如下:
public class CopyOnWriteMap<K, V> implements Map<K, V>{ private volatile Map<K, V> internalMap; private ReentrantLock lock=new ReentrantLock(); public CopyOnWriteMap() { internalMap = new HashMap<K, V>(); } //。。。帶參數的構造方法,配置HashMap public V put(K key, V value) { final ReentrantLock lock = this.lock; lock.lock(); try { Map<K,V> newMap=new HashMap(internalMap); V v=newMap.put(key,value); internalMap=newMap; return v; } finally { lock.unlock(); } } public V remove(Object key) { final ReentrantLock lock = this.lock; lock.lock(); try { Map<K, V> newMap = new HashMap<K, V>(internalMap); V val = newMap.remove(key); internalMap = newMap; return val; } finally { lock.unlock(); } } public int size() { return internalMap.size(); } public boolean isEmpty() { return internalMap.isEmpty(); } public V get(Object key) { return internalMap.get(key); } ...其他方法}
CopyOnWriteMap實現了Map介面,Map中的每個方法利用內部的HashMap實現,對於寫操作對Lock加鎖,而讀操作則不加鎖。 總結 使用情境
CopyOnWrite並發容器用於讀多寫少的並發情境。比如白名單,黑名單,商品類目的訪問和更新情境,假如有一個搜尋網站,使用者在這個網站的搜尋方塊中,輸入關鍵字搜尋內容,但是某些關鍵字不允許被搜尋。這些不能被搜尋的關鍵字會被放在一個黑名單當中,黑名單每天晚上更新一次。當使用者搜尋時,會檢查當前關鍵字在不在黑名單當中,如果在,則提示不能搜尋。 缺點
CopyOnWrite容器有很多優點,不過也有兩個缺點:
1. 記憶體佔用問題: 由於每次寫操作都會複製出一個對象出來,如果舊資料已經很大,那麼複製的資料也會很大,可能會導致過多的GC
2. 資料一致性問題: COW容器只能確保最終資料的一致性,不能保證即時資料一致性。如果你希望寫入的資料,馬上能被讀到,請不要使用COW容器。 原理
讀寫分離,寫時加鎖,讀時不加鎖。