並發包中並發List只有CopyOnWriteArrayList這一個,CopyOnWriteArrayList是一個線程安全的ArrayList,對其進行修改操作和元素迭代操作都是在底層創建一個拷貝數組(快照)上進行的,也就是寫時拷貝策略。
我們首先看一下CopyOnWriteArrayList的類圖有哪些屬性和方法,如下圖所示:

如上,CopyOnWriteArrayList的類圖,每個CopyOnWriteArrayList對象里面有一個array數組對象用來存放具體元素,ReentrantLock獨占鎖對象用來保證同時只有一個線程對array進行修改,這里只要記得ReentrantLock是獨占鎖,同時只有一個線程可以獲取就可以了。
那么問題來了,如果讓我們自己去做一個寫時拷貝的線程安全的List,我們會怎么做,要考慮哪些要點?
1.list何時初始化,初始化list元素個數為多少,list是有限大小?
2.如何保證線程安全,比如多個線程進行讀寫時候,如果保證是線程安全的?
3.如何使用迭代器遍歷list時候的數據一致性?
那么我們就進入CopyOnWriteArrayList源碼去看,看看設計者是如何處理的。
CopyOnWriteArrayList源碼分離如下:
1.初始化,首先看一下CopyOnWriteArrayList的無參構造函數,如下代碼內部創建了一個大小為0的Object數據作為array的初始值。源碼如下:
public CopyOnWriteArrayList() { setArray(new Object[0]); }
接下來再看看CopyOnWriteArrayList的有參構造函數,源碼如下:
//創建一個list,其內部元素是入參toCopyIn的拷貝 public CopyOnWriteArrayList(E[] toCopyIn) { setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class)); }
//入參為集合,拷貝集合里面元素到本list public CopyOnWriteArrayList(Collection<? extends E> c) { Object[] elements; if (c.getClass() == CopyOnWriteArrayList.class) elements = ((CopyOnWriteArrayList<?>)c).getArray(); else { elements = c.toArray(); // c.toArray might (incorrectly) not return Object[] (see 6260652) if (elements.getClass() != Object[].class) elements = Arrays.copyOf(elements, elements.length, Object[].class); } setArray(elements); }
2.添加元素,CopyOnWriteArrayList中添加元素函數有add(E e) , add(int index , E element) ,addIfAbsent(E e) , addAllAbsent(Collection<? extends E> c) 等操作,原理一致,
接下來就以add(E e)進行講解。源碼如下:
public boolean add(E e) { //加獨占鎖(1) final ReentrantLock lock = this.lock; lock.lock(); try { //獲取array(2) Object[] elements = getArray(); //拷貝array到新數組,添加元素到新數組(3) int len = elements.length; Object[] newElements = Arrays.copyOf(elements, len + 1); newElements[len] = e; //使用新數組替換添加前的數組(4) setArray(newElements); return true; } finally { //釋放獨占鎖(5) lock.unlock(); } }
如上代碼,調用add方法的線程會首先執行代碼(1)去獲取獨占鎖,如果多個線程都調用add則只有一個線程會獲取該鎖,其他線程會被阻塞掛起知道鎖被釋放。所以一個線程獲取到鎖后,就保證了在該線程添加元素過程中,其他線程不會對array進行修改。
線程獲取鎖后執行代碼(2)獲取array,然后執行代碼(3)拷貝array到一個新數組(從這里可以知道新數組的大小是原來數組大小加增加1,所以CopyOnWriteArrayList是無界List),並把要新增的元素添加到新數組。
然后執行到代碼(4)把新數組替換原數組,然后返回前要釋放鎖,由於加了鎖,所以整個add過程是個原子操作,需要注意的是添加元素時候首先是拷貝了一個快照,然后在快照上進行的添加,而不是直接在源來的數組上進行。
3.獲取指定位置元素,E get(int index)獲取下標為index的元素,如果元素不存在會拋出IndexOutOfBoundsException 異常,源碼如下:
public E get(int index) { return get(getArray(), index); } final Object[] getArray() { return array; } private E get(Object[] a, int index) { return (E) a[index]; }
}
如上代碼,獲取指定位置的元素分為兩步,首先獲取到當前list里面的array數組,這里稱為步驟1,然后通過隨機訪問的下標方式訪問指定位置的元素,這里稱為步驟2。
從代碼中可以看到整個過程並沒有加鎖,這就可能會導致當執行完步驟1后執行步驟2前,另外一個線程C進行了修改操作,比如remove操作,就會進行寫時拷貝刪除當前get方法要訪問的元素,並且修改當前list的array為新數組。
而這之后步驟2 可能才開始執行,步驟2操作的是線程C刪除元素前的一個快照數組(因為步驟1讓array指向的是原來的數組),所以雖然線程C已經刪除了index處的元素,但是步驟2還是返回index處的元素,這其實就是寫時拷貝策略帶來弱一致性。
4.修改指定元素,修改 list 中指定元素的值,如果指定位置的元素不存在則拋出 IndexOutOfBoundsException 異常,源碼碼如下:
public E set(int index, E element) { final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); E oldValue = get(elements, index); if (oldValue != element) { int len = elements.length; Object[] newElements = Arrays.copyOf(elements, len); newElements[index] = element; setArray(newElements); } else { // Not quite a no-op; ensures volatile write semantics setArray(elements); } return oldValue; } finally { lock.unlock(); } }
如上代碼,首先獲取了獨占鎖控制了其他線程對array數組的修改,然后獲取當前數組,並調用get方法獲取指定位置元素。
如果指定的位置元素與新值不一致則創建新數組並拷貝元素,在新數組上修改指定位置元素值並設置新數組到array。
如果指定位置元素與新值一樣,則為了保障volatile語義,還是需要重新設置下array,雖然array內容並沒有改變(為了保證 volatile 語義是考慮到 set 方法本身應該提供 volatile 的語義).
5.刪除元素,刪除list里面指定的元素,主要的方法有如下方法:
E remove(int index)
boolean remove(Object o)
boolean remove(Object o, Object[] snapshot, int index) 等方法,原理一致,這里講解下 remove(int index) 方法,源碼如下:
public E remove(int index) { //獲取獨占鎖 final ReentrantLock lock = this.lock; lock.lock(); try { //獲取數組 Object[] elements = getArray(); int len = elements.length; //獲取指定元素 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(); } }
正如上面代碼所示,其實和新增元素時候是類似的,首先是獲取獨占鎖保證刪除數組期間,其他線程不能對array進行修改,然后獲取數組中要給刪除的元素,並把剩余的原始拷貝到新數組后,把新數組替換原來的數組,最后在返回前釋放鎖。
6.接下來要講解一下弱一致性的迭代器。
遍歷列表元素可以使用迭代器進行迭代操作,講解什么是迭代器的弱一致性前先上一個例子說明下迭代器的使用。代碼如下:
import java.util.concurrent.CopyOnWriteArrayList; /** * Created by cong on 2018/6/9. */ public class CopyOnWriteArrayListTest { public static void main( String[] args ) { CopyOnWriteArrayList<String> arrayList = new CopyOnWriteArrayList<>(); arrayList.add("hello"); arrayList.add("java"); Iterator<String> itr = arrayList.iterator(); while(itr.hasNext()){ System.out.println(itr.next()); } } }
運行結果如下:

其中迭代器的hasNext方法用來判斷是否還有元素,next方法則是具體返回元素。那么接下來看CopyOnWriteArrayList中迭代器是弱一致性,所謂弱一致性是指返回迭代器后,其他線程對list的增刪改對迭代器不可見,無感知的,那么下面就看看是如何做到的。源碼如下:
public Iterator<E> iterator() { return new COWIterator<E>(getArray(), 0); } static final class COWIterator<E> implements ListIterator<E> { //array的快照版本 private final Object[] snapshot; //數組下標 private int cursor; //構造函數 private COWIterator(Object[] elements, int initialCursor) { cursor = initialCursor; snapshot = elements; } //是否遍歷結束 public boolean hasNext() { return cursor < snapshot.length; } //獲取元素 public E next() { if (! hasNext()) throw new NoSuchElementException(); return (E) snapshot[cursor++]; }
}
如上代碼當調用iterator()方法獲取迭代器時候實際是返回一個COWIterator對象,COWIterator的snapshot變量保存了當前list的內容,cursor是遍歷list數據的下標。
這里為什么說snapshot是list的快照呢?明明是指針傳遞的引用,而不是拷貝。如果在該線程使用返回的迭代器遍歷元素的過程中,其他線程沒有對list進行增刪改,那么snapshot本身就是list的array,因為它們是引用關系。
但是如果遍歷期間,有其他線程對該list進行了增刪改,那么snapshot就是快照了,因為增刪改后list里面的數組被新數組替換了,這時候老數組只有被snapshot索引用,所以這也就說明獲取迭代器后,使用改迭代器進行變量元素時候,其它線程對該list進行的增刪改是不可見的,
因為它們操作的是兩個不同的數組,這也就是弱一致性的達成。
下面通過一個例子來演示多線程下,迭代器的弱一致性的效果:代碼如下:
import java.util.Iterator; import java.util.concurrent.CopyOnWriteArrayList; /** * Created by cong on 2018/6/9. */ public class CopyOnWriteArrayListTest { private static volatile CopyOnWriteArrayList<String> arrayList = new CopyOnWriteArrayList<>(); public static void main( String[] args ) throws InterruptedException { arrayList.add("hello"); arrayList.add("Java"); arrayList.add("welcome"); arrayList.add("to"); arrayList.add("hangzhou"); Thread threadOne = new Thread(new Runnable() { @Override public void run() { //修改list中下標為1的元素為JavaSe arrayList.set(1, "JavaSe"); //刪除元素 arrayList.remove(2); arrayList.remove(3); } }); //保證在修改線程啟動前獲取迭代器 Iterator<String> itr = arrayList.iterator(); //啟動線程 threadOne.start(); //等在子線程執行完畢 threadOne.join(); //迭代元素 while(itr.hasNext()){ System.out.println(itr.next()); } } }
運行結果如下:

如上代碼main函數首先初始化了arrayList,然后在啟動線程前獲取到了arrayList迭代器,子線程ThreadOne啟動后首先修改了arrayList第一個元素的值,然后刪除了arrayList中坐標為2,3 的元素。
主線程等待子線程執行完畢后使用獲取的迭代器遍歷數組元素,從打印的結果來看,子線程里面進行的操縱是一個都沒有生效,這就是迭代器的弱一致性的效果,需要注意的是獲取迭代器必須在子線程操作之前進行。
注意:CopyOnWriteArrayList使用寫時拷貝的策略來保證list的一致性,而獲取-拷貝-寫入 三步並不是原子性的,所以在修改增刪改的過程中都是用了獨占鎖,並保證了同時只有一個線程才能對list數組進行修改。
另外CopyOnWriteArrayList提供了弱一致性的迭代器,保證在獲取迭代器后,其他線程對list的修改該不可見,迭代器遍歷時候的數組是獲取迭代器時候的一個快照,另外並發包中CopyOnWriteArraySet 底層就是使用它進行實現,感興趣的可以去翻翻看。
