Java Review - 并发编程_并发List_CopyOnWriteArrayList源码剖析
文章目錄
- 概述
- 源碼解析
- 初始化
- 添加元素
- 獲取指定位置元素
- 修改指定元素
- 刪除元素
- 弱一致性的迭代器
- CopyOnWriteArrayList 是如何實現弱一致性的
- 小結
概述
并發包中的并發List只有CopyOnWriteArrayList。CopyOnWriteArrayList是一個線程安全的ArrayList,對其進行的修改操作都是在底層的一個復制的數組(快照)上進行的,也就是使用了寫時復制策略。
在CopyOnWriteArrayList的類中,每個CopyOnWriteArrayList對象里面有一個array數組對象用來存放具體元素,ReentrantLock獨占鎖對象用來保證同時只有一個線程對array進行修改。
我們思考幾個問題
-
何時初始化list,初始化的list元素個數為多少,list是有限大小嗎?
-
如何保證線程安全,比如多個線程進行讀寫時如何保證是線程安全的?
-
如何保證使用迭代器遍歷list時的數據一致性?
源碼解析
初始化
首先看下無參構造函數,如下代碼在內部創建了一個大小為0的Object數組作為array的初始值。
/*** Creates an empty list.*/public CopyOnWriteArrayList() {setArray(new Object[0]);}然后看下有參構造函數。
/*** Creates a list containing the elements of the specified* collection, in the order they are returned by the collection's* iterator.** @param c the collection of initially held elements* @throws NullPointerException if the specified collection is null*/// 入參為集合,將集合中的元素復制到本Listpublic CopyOnWriteArrayList(Collection<? extends E> c) {Object[] elements;if (c.getClass() == CopyOnWriteArrayList.class)elements = ((CopyOnWriteArrayList<?>)c).getArray();else {elements = c.toArray();if (c.getClass() != ArrayList.class)elements = Arrays.copyOf(elements, elements.length, Object[].class);}setArray(elements);}/*** Creates a list holding a copy of the given array.** @param toCopyIn the array (a copy of this array is used as the* internal array)* @throws NullPointerException if the specified array is null*/// 創建一個List , 其內部元素是toCopyIn的副本public CopyOnWriteArrayList(E[] toCopyIn) {setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));}添加元素
用來添加元素的函數有 add(E e)、add(int index, E element)、addIfAbsent(E e)和addAllAbsent(Collection<? extends E> c)等 原理類似, 以add(E e)為例
- 首先執行代碼(1)去獲取獨占鎖,如果多個線程都調用add方法則只有一個線程會獲取到該鎖,其他線程會被阻塞掛起直到鎖被釋放。 所以一個線程獲取到鎖后,就保證了在該線程添加元素的過程中其他線程不會對array進行修改
- 線程獲取鎖后執行代碼(2)獲取array
- 執行代碼(3)復制array到一個新數組(從這里可以知道新數組的大小是原來數組大小增加1,所以CopyOnWriteArrayList是無界list),并把新增的元素添加到新數組
- 執行代碼(4)使用新數組替換原數組
- 執行代碼(5)返回前釋放鎖。
由于加了鎖,所以整個add過程是個原子性操作。需要注意的是,在添加元素時,首先復制了一個快照,然后在快照上進行添加,而不是直接在原來數組上進行。
獲取指定位置元素
使用E get(int index) 獲取下標為index的元素,如果元素不存在則拋出IndexOutOfBoundsException異常。
@SuppressWarnings("unchecked")private E get(Object[] a, int index) {return (E) a[index];}/*** {@inheritDoc}** @throws IndexOutOfBoundsException {@inheritDoc}*/public E get(int index) {return get(getArray(), index);}在如上代碼中,當線程x調用get方法獲取指定位置的元素時,分兩步走,首先獲取array數組(這里命名為步驟A),然后通過下標訪問指定位置的元素(這里命名為步驟B),這是兩步操作,但是在整個過程中并沒有進行加鎖同步。
由于執行步驟A和步驟B沒有加鎖,這就可能導致在線程x執行完步驟A后執行步驟B前,另外一個線程y進行了remove操作,假設要刪除元素1。
remove操作首先會獲取獨占鎖,然后進行寫時復制操作,也就是復制一份當前array數組,然后在復制的數組里面刪除線程x通過get方法要訪問的元素1,之后讓array指向復制的數組。
而這時候array之前指向的數組的引用計數為1而不是0,因為線程x還在使用它,這時線程x開始執行步驟B,步驟B操作的數組是線程y刪除元素之前的數組,如下圖所示。
所以,雖然線程y已經刪除了index處的元素,但是線程x的步驟B還是會返回index處的元素,這其實就是寫時復制策略產生的弱一致性問題。
修改指定元素
使用E set(int index,E element)修改list中指定元素的值,如果指定位置的元素不存在則拋出IndexOutOfBoundsException異常
/*** Replaces the element at the specified position in this list with the* specified element.** @throws IndexOutOfBoundsException {@inheritDoc}*/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 semanticssetArray(elements);}return oldValue;} finally {lock.unlock();}}首先獲取了獨占鎖,從而阻止其他線程對array數組進行修改,然后獲取當前數組,并調用get方法獲取指定位置的元素,如果指定位置的元素值與新值不一致則創建新數組并復制元素,然后在新數組上修改指定位置的元素值并設置新數組到array。
如果指定位置的元素值與新值一樣,則為了保證volatile語義,還是需要重新設置array,雖然array的內容并沒有改變。
刪除元素
刪除list里面指定的元素,可以使用 E remove(int index) 、 boolean remove(Object o)和boolean remove(Object o, Object[] snapshot, int index) 等方法,它們的原理一樣。
/*** Removes the element at the specified position in this list.* Shifts any subsequent elements to the left (subtracts one from their* indices). Returns the element that was removed from the list.** @throws IndexOutOfBoundsException {@inheritDoc}*/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進行修改,然后獲取數組中要被刪除的元素,并把剩余的元素復制到新數組,之后使用新數組替換原來的數組,最后在返回前釋放鎖。
弱一致性的迭代器
遍歷列表元素可以使用迭代器。在講解什么是迭代器的弱一致性前,先舉一個例子來說明如何使用迭代器。
import java.util.Iterator; import java.util.concurrent.CopyOnWriteArrayList;/*** @author 小工匠* @version 1.0* @description: TODO* @date 2021/12/1 21:07* @mark: show me the code , change the world*/ public class COWArrayListTest {public static void main(String[] args) {CopyOnWriteArrayList copyOnWriteArrayList = new CopyOnWriteArrayList<>();copyOnWriteArrayList.add("hello");copyOnWriteArrayList.add("artisan");copyOnWriteArrayList.add("learn");copyOnWriteArrayList.add("ml");Iterator iterator = copyOnWriteArrayList.iterator();while (iterator.hasNext()){System.out.println(iterator.next());}} }
迭代器的hasNext方法用于判斷列表中是否還有元素,next方法則具體返回元素。
下面來看CopyOnWriteArrayList中迭代器的弱一致性是怎么回事,所謂弱一致性是指返回迭代器后,其他線程對list的增刪改對迭代器是不可見的.
下面看個例子
import java.util.Iterator; import java.util.concurrent.CopyOnWriteArrayList;/*** @author 小工匠* @version 1.0* @description: TODO* @date 2021/12/1 21:17* @mark: show me the code , change the world*/ public class COWArrayListTest2 {private static volatile CopyOnWriteArrayList cow = new CopyOnWriteArrayList<>();public static void main(String[] args) throws InterruptedException {cow.add("hello");cow.add("artisan");cow.add("learn");cow.add("ml");Thread thread = new Thread(() -> {System.out.println("異步線程開始操作COW~");// 異步線程修改 刪除cow.set(0, "xxxx");cow.remove(1);cow.remove(2);Iterator iterator = cow.iterator();while (iterator.hasNext()) {System.out.println(iterator.next());}System.out.println("異步線程結束操作COW~");});// 一定要在異步線程啟動前獲取到 迭代器Iterator iterator = cow.iterator();// 啟動線程thread.start();// 等待子線程結束thread.join();// 遍歷while (iterator.hasNext()) {System.out.println(iterator.next());}} }
在如上代碼中,main函數首先初始化了cow,然后在啟動線程前獲取到了cow迭代器。子線程thread啟動后首先修改了cow的第1個元素的值,然后刪除了cow中下標為1和2的元素。
主線程在子線程執行完畢后使用獲取的迭代器遍歷數組元素,從輸出結果我們知道,在子線程里面進行的操作一個都沒有生效,這就是迭代器弱一致性的體現。需要注意的是,獲取迭代器的操作必須在子線程操作之前進行。
CopyOnWriteArrayList 是如何實現弱一致性的
/*** Returns an iterator over the elements in this list in proper sequence.** <p>The returned iterator provides a snapshot of the state of the list* when the iterator was constructed. No synchronization is needed while* traversing the iterator. The iterator does <em>NOT</em> support the* {@code remove} method.** @return an iterator over the elements in this list in proper sequence*/public Iterator<E> iterator() {return new COWIterator<E>(getArray(), 0);} static final class COWIterator<E> implements ListIterator<E> {/** Snapshot of the array */// array的快照版本private final Object[] snapshot;/** Index of element to be returned by subsequent call to next. */// 數組下標private int cursor;// 構造函數private COWIterator(Object[] elements, int initialCursor) {cursor = initialCursor;snapshot = elements;}// 是否遍歷結束public boolean hasNext() {return cursor < snapshot.length;}// 獲取元素@SuppressWarnings("unchecked")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進行的增刪改不可見,因為它們操作的是兩個不同的數組,這就是弱一致性。
小結
CopyOnWriteArrayList使用寫時復制的策略來保證list的一致性,而獲取—修改—寫入三步操作并不是原子性的,所以在增刪改的過程中都使用了獨占鎖,來保證在某個時間只有一個線程能對list數組進行修改。
另外CopyOnWriteArrayList提供了弱一致性的迭代器,從而保證在獲取迭代器后,其他線程對list的修改是不可見的,迭代器遍歷的數組是一個快照。
另外,CopyOnWriteArraySet的底層就是使用CopyOnWriteArrayList實現的 。
總結
以上是生活随笔為你收集整理的Java Review - 并发编程_并发List_CopyOnWriteArrayList源码剖析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java Review - 并发编程_原
- 下一篇: Java Review - 并发编程_L