在 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
在 JDK 併發包中,目前關於 List 的併發集合,只有 CopyOnWriteArrayList 一個。上面簡單介紹了 Vector 和 SynchronizdList 的粗暴實現,既然還有 CopyOnWriteArrayList,那麼它必定是和上面兩種是有區別的,做爲惟一的併發 List,它有什麼不一樣呢?ide
在探究 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耗時: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];
}
首先看到這裏是沒有任何的加鎖操做的,而獲取指定位置的元素又分爲了兩個步驟:
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。