「源码分析」CopyOnWriteArrayList 中的隐藏知识,你Get了吗?
前言
本覺 CopyOnWriteArrayList 過于簡單,尋思看名字就能知道內部的實現邏輯,所以沒有寫這篇文章的想法,最近又仔細看了下 CopyOnWriteArrayList 的源碼實現,大體邏輯沒有意外,不過還是發現很多有意思的地方,故留此篇文章分享之。
看完這篇文章你會了解到:
CopyOnWriteArrayList 的實現原理,擴容機制。
CopyOnWriteArrayList 的讀寫分離,弱一致性。
CopyOnWriteArrayList 的性能如何。
CopyOnWriteArrayList 修改元素時,為什么相同值也要重新賦值(作者 Doug Lea 這么寫都是有道理的)。
CopyOnWriteArrayList 在高版本 JDK 的實現有什么不同,為什么。
?
線程安全 List
在 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 時會再次講到。
?
CopyOnWriteArrayList
在 JDK 并發包中,目前關于 List 的并發集合,只有 CopyOnWriteArrayList 一個。上面簡單介紹了 Vector 和 SynchronizdList 的粗暴實現,既然還有 CopyOnWriteArrayList,那么它一定是和上面兩種是有區別的,作為唯一的并發 List,它有什么不同呢?
在探究 CopyOnWriteArrayList 的實現之前,我們不妨先思考一下,如果是你,你會怎么來實現一個線程安全的 List。
并發讀寫時該怎么保證線程安全呢?
數據要保證強一致性嗎?數據讀寫更新后是否立刻體現?
初始化和擴容時容量給多少呢?
遍歷時要不要保證數據的一致性呢?需要引入 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);?//?拷貝一個數據數組,長度+1newElements[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?semanticssetArray(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("AAA");Iterator<String>?iterator?=?list.iterator(); list.add("java"); while?(iterator.hasNext())?{String?next?=?iterator.next();System.out.println(next); }現在 List 中添加了元素 www.wdbyte.com 和 AAA,在拿到迭代器對象后,又添加了新元素 java ,可以看到遍歷的結果沒有報錯也沒有輸出 java 。也就是說拿到迭代器對象后,元素的更新不可見。
www.wdbyte.com AAA這是為什么呢?要先從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?==?elementsetArray(es);return?oldValue;} }?
總結
通過上面的分析,得到下面幾點關于 CopyOnWriteArrayList 的總結。
CopyOnWriteArrayList 采用讀寫分離,寫時復制方式實現線程安全,具有弱一致性。
CopyOnWriteArrayList 因為每次寫入時都要擴容復制數組,寫入性能不佳。
CopyOnWriteArrayList 在修改元素時,為了保證 volatile 語義,即使元素沒有任何變化也會重新賦值,
在高版 JDK 中,得益于 synchronized 鎖升級策略, CopyOnWriteArrayList 的加鎖方式采用了 synchronized。
參考:
Why setArray() method call required in CopyOnWriteArrayList.
https://stackoverflow.com/questions/28772539/why-setarray-method-call-required-in-copyonwritearraylist
What does volatile do?
http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#volatile
有道無術,術可成;有術無道,止于術
歡迎大家關注Java之道公眾號
好文章,我在看??
總結
以上是生活随笔為你收集整理的「源码分析」CopyOnWriteArrayList 中的隐藏知识,你Get了吗?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 通过一条语句的执行,深入理解innoDB
- 下一篇: uni-app-页面结构