寫時複製,CopyOnWriteArrayList原始碼剖析
寫時複製,CopyOnWriteArrayList原始碼剖析
1、CopyOnWriteArrayList介紹
CopyOnWriteArrayList是一個執行緒安全的ArrayList,對其進行的修改操作都是在底層的一個複製的陣列(快照)上進行的,也就是使用了寫時複製策略。
每個 CopyOnWriteArrayList物件裡面有一個array陣列物件用來存放具體元素,ReentrantLock獨佔鎖物件用來保證同時只有一個執行緒對array進行修改。
寫時複製,CopyOnWrite容器即寫時複製的容器,往一個容器中新增元素的時候,不直接往當前容器Object[]新增,而是先將Object[]進行copy,複製出一個新的容器object[] newElements,然後新的容器Object[] newElements裡新增原始,新增元素完後,在將原容器的引用指向新的容器 setArray(newElements);這樣做的好處是可以對copyOnWrite容器進行併發的讀 ,而不需要加鎖,因為當前容器不需要新增任何元素。所以CopyOnWrite容器也是一種讀寫分離的思想,讀和寫不同的容器。
2、CopyOnWriteArrayList原始碼剖析
初始化
public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { private static final long serialVersionUID = 8673264195747942595L; /** The lock protecting all mutators */ final transient ReentrantLock lock = new ReentrantLock(); /** The array, accessed only via getArray/setArray. */ private transient volatile Object[] array; }
構造一個Object型別陣列
final void setArray(Object[] a) {
array = a;
}
無參建構函式,在內部建立了一個大小為0的Object陣列作為array的初始值。
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
入參為集合,將集合裡面的元素複製到本list
public CopyOnWriteArrayList(Collection<? extends E> c) { Object[] elements; if (c.getClass() == CopyOnWriteArrayList.class) elements = ((CopyOnWriteArrayList<?>)c).getArray(); else { elements = c.toArray(); if (c.getClass() != ArrayList.class) elements = Arrays.copyOf(elements, elements.length, Object[].class); } setArray(elements); }
建立一個list,其內部元素是入參toCopyIn的副本
public CopyOnWriteArrayList(E[] toCopyIn) {
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}
新增元素
public boolean add(E e) {
// 獲取獨佔鎖
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 獲取array
Object[] elements = getArray();
// 獲取array的陣列長度,並將其複製到新陣列newElements中,陣列長度+1,將新添的值新增到新陣列中
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
// 使用新陣列替換新增前的陣列
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
如上程式碼所示,呼叫add方法的程式碼會首先去獲取獨佔鎖lock,如果多個執行緒都呼叫add方法,則只有一個執行緒會獲取到該鎖,其他執行緒會被阻塞掛起,直到鎖被釋放。所以一個執行緒獲取到鎖後,就保證了該執行緒新增元素的過程中其他執行緒不會對array進行修改。
執行緒獲取鎖後再執行程式碼獲取陣列array,然後複製array到一個新陣列,新陣列的大小是原來陣列的大小加1,所以CopyOnWriteList是一個無界List,並把新增的元素新增到新陣列。
然後使用新陣列替換原陣列,並在返回前釋放鎖,由於加了鎖,所以整個add過程是個原子性操作。再新增元素時,首先複製了一個快照,然後在快照上進行新增,而不是直接在原來陣列上進行。
// 返回array陣列
final Object[] getArray() {
return array;
}
獲取指定位置元素
public E get(int index) {
return get(getArray(), index);
}
private E get(Object[] a, int index) {
return (E) a[index];
}
final Object[] getArray() {
return array;
}
由於get操作沒有加鎖,所以最終拿到的資料不一定是最新的資料。在如上程式碼中,執行緒x呼叫get方法獲取指定位置的元素時分兩步走,首先獲取array陣列,然後再通過下標訪問指定位置的元素,如果在獲取指定位置元素前,有另外一個執行緒y對陣列進行了刪除操作,則這時執行緒x還是隻想原始陣列array,而此時堆記憶體的陣列已經變為了newarray,所以這就是寫時複製策略產生的弱一致性問題。
修改指定元素
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();
}
}
如上程式碼,首先獲取獨佔鎖lock,從而組織了其他執行緒對array陣列進行修改、新增、刪除等操作,然後獲取到當前陣列,並呼叫get方法獲取指定位置的元素,如果指定位置的元素值和新值不一致則建立新陣列並複製元素,然後新陣列上修改指定位置的元素值並設定新陣列到array。如果指定位置的元素值與新值一樣,則為了保證volatile語義,還需要重新設定array,雖然array的內容並沒有改變。
刪除元素
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();
}
}
刪除元素程式碼和新增元素的程式碼類似,首先獲取獨佔鎖以保證刪除資料期間其他執行緒不能對array進行修改,然後獲取陣列中要被刪除的元素,並把剩餘的元素複製到新陣列,之後使用新陣列替換原來的陣列,最後在返回前釋放。