CopyOnWriteArrayList原理
文章目錄
- CopyOnWriteArrayList原理
-
- 1、什麼是CopyOnWrite容器
- 2、CopyOnWriteArrayList的實作原理
- 3、CopyOnWrite的缺點
1、什麼是CopyOnWrite容器
CopyOnWrite容器即寫時複制的容器。通俗的了解是當我們往一個容器添加元素的時候,不直接往目前容器添加,而是先将目前容器進行Copy,複制出一個新的容器,然後新的容器裡添加元素,添加完元素之後,再将原容器的引用指向新的容器。這樣做的好處是我們可以對CopyOnWrite容器進行并發的讀,而不需要加鎖,因為目前容器不會添加任何元素。是以CopyOnWrite容器也是一種讀寫分離的思想,讀和寫不同的容器。
适用場景:讀多寫少的場景。
2、CopyOnWriteArrayList的實作原理
在使用CopyOnWriteArrayList之前,我們先閱讀其源碼了解下它是如何實作的。以下代碼是向CopyOnWriteArrayList中add方法的實作(向CopyOnWriteArrayList裡添加元素),可以發現在添加的時候是需要加鎖的,否則多線程寫的時候會Copy出N個副本出來。【以下源碼基于jdk1.8】
/**
* 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);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
/**
* Removes the element at the specified position in this list.
* Shifts any subsequent elements to the left (subtracts one from their
* indices). Returns the element that was removed from the list.
*
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
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();
}
}
/**
* Replaces the element at the specified position in this list with the
* specified element.
*
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
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();
}
}
讀的時候不需要加鎖,如果讀的時候有多個線程正在向CopyOnWriteArrayList添加資料,讀還是會讀到舊的資料,因為開始讀的那一刻已經确定了讀的對象是舊對象。
CopyOnWrite并發容器用于讀多寫少的并發場景。比如白名單,黑名單等場景。
3、CopyOnWrite的缺點
(1)記憶體占用問題。因為CopyOnWrite的寫時複制機制,是以在進行寫操作的時候,記憶體裡會同時駐紮兩個對象的記憶體,舊的對象和新寫入的對象(注意:在複制的時候隻是複制容器裡的引用,隻是在寫的時候會建立新對象添加到新容器裡,而舊容器的對象還在使用,是以有兩份對象記憶體)。如果這些對象占用的記憶體比較大,比如說200M左右,那麼再寫入100M資料進去,記憶體就會占用300M,那麼這個時候很有可能造成頻繁的Yong GC和Full GC。之前我們系統中使用了一個服務由于每晚使用CopyOnWrite機制更新大對象,造成了每晚15秒的Full GC,應用響應時間也随之變長。
針對記憶體占用問題,可以通過壓縮容器中的元素的方法來減少大對象的記憶體消耗,比如,如果元素全是10進制的數字,可以考慮把它壓縮成36進制或64進制。或者不使用CopyOnWrite容器,而使用其他的并發容器,如ConcurrentHashMap。
(2)資料一緻性問題。CopyOnWrite容器隻能保證資料的最終一緻性,不能保證資料的實時一緻性。是以如果你希望寫入的的資料,馬上能讀到,請不要使用CopyOnWrite容器。【當執行add或remove操作沒完成時,get擷取的仍
CopyOnWriteArrayList讀取時不加鎖隻是寫入和删除時加鎖,是以一個線程X讀取的時候另一個線程Y可能執行remove操作。remove操作首先要擷取獨占鎖,然後進行寫時複制操作,就是複制一份目前的array數組,然後在複制的新數組裡面删除線程X通過get通路的元素,比如:1。删除完成後讓array指向這個新的數組。
線上程x執行get操作的時候并不是直接通過全局array通路數組元素而是通過方法的形參a通路的,a指向的位址和array指向的位址在調用get方法的那一刻是一樣的,都指向了堆記憶體的數組對象。之後改變array指向的位址并不影響get的通路,因為在調用get方法的那一刻形參a指向的記憶體位址就已經确定了,不會改變。是以讀的仍然是舊數組。然是舊數組的元素】