前言
CopyOnWriteArrayList是一個執行緒安全集合,原理簡單說就是:在保證執行緒安全的前提下,犧牲掉寫操作的效率來保證讀操作的高效。所謂CopyOnWrite就是通過複製的方式來完成對資料的修改,在進行修改的時候,複製一個新陣列,在新陣列上面進行修改操作,這樣就保證了不改變老陣列,也就沒有一寫多讀資料不一致的問題了。
具體的實現來看原始碼,JDK 8。
CopyOnWriteArrayList
定義
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
複製程式碼
在定義上和ArrayList大差不差,不過多解釋,有興趣可以看之前關於ArrayList的文章。
屬性
一個是Lock,另一個是一個物件陣列。
/** The lock protecting all mutators */
//一把鎖
transient final ReentrantLock lock = new ReentrantLock();
/** The array, accessed only via getArray/setArray. */
//一個物件陣列,只從方法getArray/setArray處接受值
//volatile後面會有專門的文章來說明
private volatile transient Object[] array;
複製程式碼
初始化
CopyOnWriteArrayList的初始化容量是0,分為這樣的幾個步驟。
//在無參構造方法中會呼叫setArray方法,引數是一個空的物件陣列,然後通過setArray把這個空的陣列賦值給屬性array
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
final void setArray(Object[] a) {
array = a;
}
複製程式碼
需要說明的是另一個有參構造方法,引數可以是一個集合
//按照集合的迭代器返回的順序建立一個包含指定集合元素的列表
public CopyOnWriteArrayList(Collection<? extends E> c) {
//將集合轉為陣列
Object[] elements = c.toArray();
//elements不能夠是一個空的物件陣列 為什麼要if這樣一個條件嘞 因為屬性中需要賦值的是一個物件陣列 所以如果if成立執行的就是把原陣列變為一個物件陣列 如果本身就是物件陣列也就不用轉了
if (elements.getClass() != Object[].class)
elements = Arrays.copyOf(elements, elements.length, Object[].class);
//賦值給屬性
setArray(elements);
}
複製程式碼
方法
add(E e)
新增一個新元素到list的尾部。
public boolean add(E e) {
//鎖 1.5新版本的鎖 已經不用synchronized了
final ReentrantLock lock = this.lock;
//加鎖
lock.lock();
try {
//getArray獲取屬性值 就是老陣列
Object[] elements = getArray();
int len = elements.length;
//這裡是重點 在這裡 複製老陣列得到了一個長度+1的新陣列
Object[] newElements = Arrays.copyOf(elements, len + 1);
//新增元素
newElements[len] = e;
//用新陣列取代老陣列
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
複製程式碼
從add方法中我們可以看到所謂的CopyOnWrite是如何實現的,在需要修改的時候,複製一個新陣列,在新陣列上修改,修改結束取代老陣列,這樣保證了修改操作不影響老陣列的正常讀取,另修改操作是加鎖的,也就是說沒有了執行緒不安全的問題。
和ArrayList相比較,效率比較低,只新增一個元素的情況下(初始容量均為0),用時是ArrayList的5倍左右,但是隨著CopyOnWriteArrayList中元素的增加,CopyOnWriteArrayList的修改代價將越來越昂貴。
除了新增其他的修改操作也都是這樣的套路,不做過多解釋,如remove,也是加鎖,複製新陣列。
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();
}
}
複製程式碼
#####get
public E get(int index) {
return get(getArray(), index);
}
//按照下標獲取陣列中對應的元素
private E get(Object[] a, int index) {
return (E) a[index];
}
複製程式碼
讀取的方法就很簡單了,按照下標獲取對應的元素。
CopyOnWriteArrayList總結
- 讀寫分離,我們修改的是新陣列,讀取的是老陣列,不是一個物件,實現了讀寫分離。這種技術資料庫用的非常多,在高併發下為了緩解資料庫的壓力,即使做了快取也要對資料庫做讀寫分離,讀的時候使用讀庫,寫的時候使用寫庫,然後讀庫、寫庫之間進行一定的同步,這樣就避免同一個庫上讀、寫的IO操作太多。
- 場景:讀操作遠多於修改操作
我不能保證每一個地方都是對的,但是可以保證每一句話,每一行程式碼都是經過推敲和斟酌的。希望每一篇文章背後都是自己追求純粹技術人生的態度。
永遠相信美好的事情即將發生。