CopyOnWriteArrayList 中的隱藏的知識,你Get了嗎?

線程安全 List

在 Java 中,線程安全的 List 不止一個,除了今天的主角CopyOnWriteArrayList 以外,還有 Vector 類和 SynchronizedList 類,它們都是線程安全的 List 集合。在介紹 CopyOnWriteArrayList 以前,先簡單介紹下另外兩個。css

若是你嘗試你查看它們的源碼,你會發現有點不對頭,併發集合不都是在 java.util.concurrent包中嘛,爲何Vector 類和 SynchronizedList 類 這兩個是在 java.util 包裏呢?java

確實是這樣的,這兩個線程安全的 List 和線程安全的 HashTable 是同樣的,都是比較簡單粗暴的實現方式,直接方法上增長 synchronized 關鍵字實現的,並且無論增刪改查,通通加上,即便是 get 方法也不例外,沒錯,就是這麼粗暴。數組

Vector 類的 get 方法:安全

// Vector 中的 get 操做添加了 synchronizedpublic synchronized E get(int index) {    if (index >= elementCount)        throw new ArrayIndexOutOfBoundsException(index);    return elementData(index);
}

SynchronizedList 類的 ge t 方法:併發

public E get(int index) {
   synchronized (mutex) {return list.get(index);}
}

同窗不妨思考一下,其實在 get 方法上添加同步機制也是有緣由的,雖然下降了效率,可是可讓寫入的數據當即能夠被查詢到,這也保證了數據的強一致性。另外上面關於 synchronized 簡單粗暴的描述也是不夠準確的,由於在高版本的 JDK 中,synchronized 已經能夠根據運行時狀況,自動調整鎖的粒度,後面介紹 CopyOnWriteArrayList 時會再次講到。app

CopyOnWriteArrayList

在 JDK 併發包中,目前關於 List 的併發集合,只有 CopyOnWriteArrayList 一個。上面簡單介紹了 Vector 和 SynchronizdList 的粗暴實現,既然還有 CopyOnWriteArrayList,那麼它必定是和上面兩種是有區別的,做爲惟一的併發 List,它有什麼不一樣呢?ide

在探究 CopyOnWriteArrayList 的實現以前,咱們不妨先思考一下,若是是你,你會怎麼來實現一個線程安全的 List。函數

  1. 併發讀寫時該怎麼保證線程安全呢?源碼分析

  2. 數據要保證強一致性嗎?數據讀寫更新後是否馬上體現?性能

  3. 初始化和擴容時容量給多少呢?

  4. 遍歷時要不要保證數據的一致性呢?須要引入 Fail-Fast 機制嗎?

經過類名咱們大體能夠猜想到 CopyOnWriteArrayList 類的實現思路:Copy-On-Write, 也就是寫時複製策略;末尾的 ArrayList 表示數據存放在一個數組裏。在對元素進行增刪改時,先把現有的數據數組拷貝一份,而後增刪改都在這個拷貝數組上進行,操做完成後再把原有的數據數組替換成新數組。這樣就完成了更新操做。

可是這種寫入時複製的方式一定會有一個問題,由於每次更新都是用一個新數組替換掉老的數組,若是不巧在更新時有一個線程正在讀取數據,那麼讀取到的就是老數組中的老數據。其實這也是讀寫分離的思想,放棄數據的強一致性來換取性能的提高。

分析源碼 ( JDK8 )

上面已經說了,CopyOnWriteArrayList 的思想是寫時複製,讀寫分離,它的內部維護着一個使用 volatile 修飾的數組,用來存放元素數據。

/** The array, accessed only via getArray/setArray. */private transient volatile Object[] array;

CopyOnWriteArrayList 類中方法不少,這裏不會一一介紹,下面會分析其中的幾個經常使用的方法,這幾個方法理解後基本就能夠掌握 CopyOnWriteArrayList 的實現原理。

構造函數

CopyOnWriteArrayList 的構造函數一共有三個,一個是無參構造,直接初始化數組長度爲0;另外兩個傳入一個集合或者數組做爲參數,而後會把集合或者數組中的元素直接提取出來賦值給 CopyOnWriteArrayList 內部維護的數組。

// 直接初始化一個長度爲 0 的數組
public CopyOnWriteArrayList() {
   setArray(new Object[0]);
}
// 傳入一個集合,提取集合中的元素賦值到 CopyOnWriteArrayList 數組
public CopyOnWriteArrayList(Collection<!--? extends E--> c) {
   Object[] es;
   if (c.getClass() == CopyOnWriteArrayList.class)
       es = ((CopyOnWriteArrayList<!--?-->)c).getArray();
   else {
       es = c.toArray();
       if (c.getClass() != java.util.ArrayList.class)
           es = Arrays.copyOf(es, es.length, Object[].class);
   }
   setArray(es);
}
// 傳入一個數組,數組元素提取後賦值到 CopyOnWriteArrayList 數組
public CopyOnWriteArrayList(E[] toCopyIn) {
   setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}

構造函數是實例建立時調用的,沒有線程安全問題,因此構造方法都是簡單的賦值操做,沒有特殊的邏輯處理。

新增元素

元素新增根據入參的不一樣有好幾個,可是原理都是同樣的,因此下面只貼出了 add(E e ) 的實現方式,是經過一個 ReentrantLock 鎖保證線程安全的。

/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
*/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); // 拷貝一個數據數組,長度+1
       newElements[len] = e; // 加入新元素
       setArray(newElements); // 用新數組替換掉老數組
       return true;
   } finally {        lock.unlock();
   }
}

具體步驟:

  1. 加鎖,獲取目前的數據數組開始操做(加鎖保證了同一時刻只有一個線程進行增長/刪除/修改操做)。

  2. 拷貝目前的數據數組,且長度增長一。

  3. 新數組中放入新的元素。

  4. 用新數組替換掉老的數組。

  5. finally 釋放鎖。

因爲每次 add 時容量只增長了1,因此每次增長時都要建立新的數組進行數據複製,操做完成後再替換掉老的數據,這必然會下降數據新增時候的性能。下面經過一個簡單的例子測試 CopyOnWriteArrayList 、Vector、ArrayList 的新增和查詢性能。

public static void main(String[] args) {
   CopyOnWriteArrayList<object> copyOnWriteArrayList = new CopyOnWriteArrayList<>();
   Vector vector = new Vector<>();
   ArrayList arrayList = new ArrayList();

   add(copyOnWriteArrayList);
   add(vector);
   add(arrayList);

   get(copyOnWriteArrayList);
   get(vector);
   get(arrayList);
}public static void add(List list) {    long start = System.currentTimeMillis();    for (int i = 0; i < 100000; i++) {        list.add(i);
   }    long end = System.currentTimeMillis();
   System.out.println(list.getClass().getName() + ".size=" + list.size() + ",add耗時:" + (end - start) + "ms");
}public static void get(List list) {    long start = System.currentTimeMillis();    for (int i = 0; i < list.size(); i++) {
       Object object = list.get(i);
   }    long end = System.currentTimeMillis();
   System.out.println(list.getClass().getName() + ".size=" + list.size() + ",get耗時:" + (end - start) + "ms");
}

從測得的結果中能夠看到 CopyOnWriteArrayList 的新增耗時最久,其次是加鎖的 Vector(Vector 的擴容默認是兩倍)。而在獲取時最快的是線程不安全的 ArrayList,其次是 CopyOnWriteArrayList,而 Vector 由於 Get 時加鎖,性能最低。

java.util.concurrent.CopyOnWriteArrayList.size=100000,add耗時:2756msjava.util.Vector.size=100000,add耗時:4msjava.util.ArrayList.size=100000,add耗時:3msjava.util.concurrent.CopyOnWriteArrayList.size=100000,get耗時:4msjava.util.Vector.size=100000,get耗時:5msjava.util.ArrayList.size=100000,get耗時:2ms

修改元素

修改元素和新增元素的思想是一致的,經過 ReentrantLock 鎖保證線程安全性,實現代碼也比較簡單,原本不許備寫進來的,可是在看源碼時發現一個很是有意思的地方,看下面的代碼。

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();
   }
}

經過源碼能夠看到在修改元素前會先比較修改先後的值是否相等,而在相等的狀況下,依舊 setArray(elements); 這就很奇妙了,究竟是爲何呢?想了解其中的緣由須要瞭解下 volatile 的特殊做用,經過下面這個代碼例子說明。

// initial conditionsint nonVolatileField = 0;
CopyOnWriteArrayList<string> list = /* a single String */// Thread 1nonVolatileField = 1;                 // (1)list.set(0, "x");                     // (2)// Thread 2String s = list.get(0);               // (3)if (s == "x") {    int localVar = nonVolatileField;  // (4)}// 例子來自:https://stackoverflow.com/questions/28772539/why-setarray-method-call-required-in-copyonwritearraylist

要想理解例子中的特殊之處,首先你要知道 volatile 能夠防止指令重排,其次要了解 happens-before 機制。說簡單點就是它們能夠保證代碼的執行先後順序。

好比上面例子中的代碼,1 會在 2 以前執行,3 會在 4 以前執行,這都沒有疑問。還有一條是 volatile 修飾的屬性寫會在讀以前執行,因此 2會在 3 以前執行。而執行順序還存在傳遞性。因此最終 1 會在 4 以前執行。這樣 4 獲取到的值就是步驟 1 爲 nonVolatileField 賦的值。若是 CopyOnWriteArrayList 中的 set 方法內沒有爲相同的值進行 setArray,那麼上面說的這些就都不存在了。

刪除元素

remove 刪除元素方法一共有三個,這裏只看public E 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(); // 解鎖
   }
}

代碼仍是很簡單的,使用 ReentrantLock 獨佔鎖保證操做的線程安全性,而後使用刪除元素後的剩餘數組元素拷貝到新數組,使用新數組替換老數組完成元素刪除,最後釋放鎖返回。

獲取元素

獲取下標爲 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];
}

首先看到這裏是沒有任何的加鎖操做的,而獲取指定位置的元素又分爲了兩個步驟:

  1. getArray() 獲取數據數組。

  2. get(Object[] a, int index) 返回指定位置的元素。

頗有可能在第一步執行完成以後,步驟二執行以前,有線程對數組進行了更新操做。經過上面的分析咱們知道更新會生成一個新的數組,而咱們第一步已經獲取了老數組,因此咱們在進行 get 時依舊在老數組上進行,也就是說另外一個線程的更新結果沒有對咱們的本次 get 生效。這也是上面提到的弱一致性問題。

迭代器的弱一致性

List<string> list = new CopyOnWriteArrayList<>();list.add("www.wdbyte.com");list.add("未讀代碼");

Iterator<string> iterator = list.iterator();list.add("java");while (iterator.hasNext()) {
   String next = iterator.next();
   System.out.println(next);
}

如今 List 中添加了元素 www.wdbyte.com 和 未讀代碼,在拿到迭代器對象後,又添加了新元素 java ,能夠看到遍歷的結果沒有報錯也沒有輸出 java 。也就是說拿到迭代器對象後,元素的更新不可見。

www.wdbyte.com未讀代碼

這是爲何呢?要先從CopyOnWriteArrayList 的 iterator() 方法的實現看起。

public Iterator<e> iterator() {    return new COWIterator<e>(getArray(), 0);
}static final class COWIterator<e> implements ListIterator<e> {
   /** Snapshot of the 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;
   }
......

能夠看到在獲取迭代器時,先 getArray() 拿到了數據數組 而後傳入到 COWIterator 構造器中,接着賦值給了COWIterator 中的 snapshot 屬性,結合上面的分析結果,能夠知道每次更新都會產生新的數組,而這裏使用的依舊是老數組,因此更新操做不可見,也就是上面屢次提到的弱一致性。

新版變化

上面的源碼分析都是基於 JDK 8 進行的。寫文章時順便看了下新版的實現方式有沒有變化,還真的有挺大的改變,主要體如今加鎖的方式上,或許是由於 JVM 後來引入了 synchronized 鎖升級策略,讓 synchronized 性能有了很多提高,因此用了 synchronized 鎖替換了老的 ReentrantLock 鎖。

新增:

public boolean add(E e) {    synchronized (lock) {
       Object[] es = getArray();        int len = es.length;
       es = Arrays.copyOf(es, len + 1);
       es[len] = e;
       setArray(es);        return true;
   }
}

修改:

public E set(int index, E element) {
   synchronized (lock) {
       Object[] es = getArray();
       E oldValue = elementAt(es, index);        if (oldValue != element) {
           es = es.clone();
           es[index] = element;
       }
       // Ensure volatile write semantics even when oldvalue == element
       setArray(es);        return oldValue;
   }
}

總結

經過上面的分析,獲得下面幾點關於 CopyOnWriteArrayList 的總結。

  1. CopyOnWriteArrayList 採用讀寫分離,寫時複製方式實現線程安全,具備弱一致性。

  2. CopyOnWriteArrayList 由於每次寫入時都要擴容複製數組,寫入性能不佳。

  3. CopyOnWriteArrayList 在修改元素時,爲了保證 volatile 語義,即便元素沒有任何變化也會從新賦值,

  4. 在高版 JDK 中,得益於 synchronized 鎖升級策略, CopyOnWriteArrayList 的加鎖方式採用了 synchronized。

相關文章
相關標籤/搜索