您好,登錄后才能下訂單哦!
本篇內容主要講解“CopyOnWriteArrayList中的隱藏的知識點有哪些”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“CopyOnWriteArrayList中的隱藏的知識點有哪些”吧!
在 Java 中,線程安全的 List 不止一個,除了今天的主角 CopyOnWriteArrayList 之外,還有 Vector 類和 SynchronizedList 類,它們都是線程安全的 List 集合。在介紹 CopyOnWriteArrayList 之前,先簡單介紹下另外兩個。
如果你嘗試你查看它們的源碼,你會發現有點不對頭,并發集合不都是在 java.util.concurrent
包中嘛,為什么Vector 類和 SynchronizedList 類 這兩個是在 java.util
包里呢?
確實是這樣的,這兩個線程安全的 List 和線程安全的 HashTable 是一樣的,都是比較簡單粗暴的實現方式,直接方法上增加 synchronized
關鍵字實現的,而且不管增刪改查,統統加上,即使是 get 方法也不例外,沒錯,就是這么粗暴。
Vector 類的 get 方法:
// Vector 中的 get 操作添加了 synchronized public 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 時會再次講到。
在 JDK 并發包中,目前關于 List 的并發集合,只有 CopyOnWriteArrayList 一個。上面簡單介紹了 Vector 和 SynchronizdList 的粗暴實現,既然還有 CopyOnWriteArrayList,那么它一定是和上面兩種是有區別的,作為唯一的并發 List,它有什么不同呢?
在探究 CopyOnWriteArrayList 的實現之前,我們不妨先思考一下,如果是你,你會怎么來實現一個線程安全的 List。
并發讀寫時該怎么保證線程安全呢?
數據要保證強一致性嗎?數據讀寫更新后是否立刻體現?
初始化和擴容時容量給多少呢?
遍歷時要不要保證數據的一致性呢?需要引入 Fail-Fast 機制嗎?
通過類名我們大致可以猜測到 CopyOnWriteArrayList 類的實現思路:Copy-On-Write, 也就是寫時復制策略;末尾的 ArrayList 表示數據存放在一個數組里。在對元素進行增刪改時,先把現有的數據數組拷貝一份,然后增刪改都在這個拷貝數組上進行,操作完成后再把原有的數據數組替換成新數組。這樣就完成了更新操作。
但是這種寫入時復制的方式必定會有一個問題,因為每次更新都是用一個新數組替換掉老的數組,如果不巧在更新時有一個線程正在讀取數據,那么讀取到的就是老數組中的老數據。其實這也是讀寫分離的思想,放棄數據的強一致性來換取性能的提升。
上面已經說了,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(); } }
具體步驟:
加鎖,獲取目前的數據數組開始操作(加鎖保證了同一時刻只有一個線程進行增加/刪除/修改操作)。
拷貝目前的數據數組,且長度增加一。
新數組中放入新的元素。
用新數組替換掉老的數組。
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耗時:2756ms java.util.Vector.size=100000,add耗時:4ms java.util.ArrayList.size=100000,add耗時:3ms java.util.concurrent.CopyOnWriteArrayList.size=100000,get耗時:4ms java.util.Vector.size=100000,get耗時:5ms java.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 conditions int nonVolatileField = 0; CopyOnWriteArrayList<string> list = /* a single String */ // Thread 1 nonVolatileField = 1; // (1) list.set(0, "x"); // (2) // Thread 2 String 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]; }
首先看到這里是沒有任何的加鎖操作的,而獲取指定位置的元素又分為了兩個步驟:
getArray() 獲取數據數組。
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 的總結。
CopyOnWriteArrayList 采用讀寫分離,寫時復制方式實現線程安全,具有弱一致性。
CopyOnWriteArrayList 因為每次寫入時都要擴容復制數組,寫入性能不佳。
CopyOnWriteArrayList 在修改元素時,為了保證 volatile 語義,即使元素沒有任何變化也會重新賦值,
在高版 JDK 中,得益于 synchronized 鎖升級策略, CopyOnWriteArrayList 的加鎖方式采用了 synchronized。
到此,相信大家對“CopyOnWriteArrayList中的隱藏的知識點有哪些”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。