1、CopyOnWrite容器(並發容器)
Copy-On-Write簡稱COW,是一種用於程序設計中的優化策略。
其基本思路是,從一開始大家都在共享同一個內容,當某個人想要修改這個內容的時候,才會真正把內容Copy出去形成一個新的內容然后再改,這是一種延時懶惰策略。
從JDK1.5開始Java並發包里提供了兩個使用CopyOnWrite機制實現的並發容器,它們是CopyOnWriteArrayList和CopyOnWriteArraySet。
CopyOnWrite容器即寫時復制的容器。
通俗的理解是當我們往一個容器添加元素的時候,不直接往當前容器添加,而是先將當前容器進行Copy,復制出一個新的容器,然后新的容器里添加元素,添加完元素之后,再將原容器的引用指向新的容器。
這樣做的好處是我們可以對CopyOnWrite容器進行並發的讀,而不需要加鎖,因為當前容器不會添加任何元素。
所以CopyOnWrite容器是一種讀寫分離的思想,讀和寫不同的容器、最終一致性 以及使用另外開辟空間的思路,來解決並發沖突的思想。
2、CopyOnWriteArrayList數據結構
public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {}
CopyOnWriteArrayList實現了List接口,List接口定義了對列表的基本操作;
- 同時實現了RandomAccess接口,表示可以隨機訪問(數組具有隨機訪問的特性);
- 同時實現了Cloneable接口,表示可克隆;
- 同時也實現了Serializable接口,表示可被序列化。
- CopyOnWriteArrayList底層使用數組來存放元素。
2、CopyOnWriteArrayList Add方法
CopyOnWriteArrayList容器是Collections.synchronizedList(List list)的替代方案,是一個ArrayList的線程安全的變體。
基本原理:
初始化的時候只有一個容器,很常一段時間,這個容器數據、數量等沒有發生變化的時候,大家(多個線程),都是讀取(假設這段時間里只發生讀取的操作)同一個容器中的數據,所以這樣大家讀到的數據都是唯一、一致、安全的,但是后來有人往里面增加了一個數據,這個時候CopyOnWriteArrayList 底層實現添加的原理是先copy出一個容器(可以簡稱副本),再往新的容器里添加這個新的數據,最后把新的容器的引用地址賦值給了之前那個舊的的容器地址,但是在添加這個數據的期間,其他線程如果要去讀取數據,仍然是讀取到舊的容器里的數據。
CopyOnWriteArrayList中add方法的實現(向CopyOnWriteArrayList里添加元素),可以發現在添加的時候是需要加鎖的,否則多線程寫的時候會Copy出N個副本出來。
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();
}
}
/**
* Sets the array.
*/
final void setArray(Object[] a) {
array = a;
}
讀的時候不需要加鎖,如果讀的時候有多個線程正在向CopyOnWriteArrayList添加數據,讀還是會讀到舊的數據,因為寫的時候不會鎖住舊的CopyOnWriteArrayList。
public E get(int index) { return get(getArray(), index); }
3、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(); } }
刪除元素,很簡單,就是判斷要刪除的元素是否最后一個,如果最后一個直接在復制副本數組的時候,復制長度為舊數組的length-1即可;
但是如果不是最后一個元素,就先復制舊的數組的index前面元素到新數組中,然后再復制舊數組中index后面的元素到數組中,最后再把新數組復制給舊數組的引用。最后在finally語句塊中將鎖釋放。
4、set方法
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(); } }
5、CopyOnWriteArrayList初始化(構造方法)
/** * Sets the array.把老數組指向新數組么 */ final void setArray(Object[] a) { array = a; } /** * Creates an empty list.構造函數 */ public CopyOnWriteArrayList() { setArray(new Object[0]); } public CopyOnWriteArrayList(Collection<? extends E> c) { Object[] elements = c.toArray(); // c.toArray might (incorrectly) not return Object[] (see 6260652) if (elements.getClass() != Object[].class) elements = Arrays.copyOf(elements, elements.length, Object[].class); setArray(elements); } /** * Creates a list holding a copy of the given array. * * @param toCopyIn the array (a copy of this array is used as the * internal array) * @throws NullPointerException if the specified array is null */ public CopyOnWriteArrayList(E[] toCopyIn) { setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class)); }
無論我們用哪一個構造方法創建一個CopyOnWriteArrayList對象,都會創建一個Object類型的數組,然后賦值給成員array。
6、copyOf函數
該函數用於復制指定的數組,截取或用 null 填充(如有必要),以使副本具有指定的長度。
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) { @SuppressWarnings("unchecked") // 確定copy的類型(將newType轉化為Object類型,將Object[].class轉化為Object類型,判斷兩者是否相等,若相等,則生成指定長度的Object數組 // 否則,生成指定長度的新類型的數組) T[] copy = ((Object)newType == (Object)Object[].class) ? (T[]) new Object[newLength] : (T[]) Array.newInstance(newType.getComponentType(), newLength); // 將original數組從下標0開始,復制長度為(original.length和newLength的較小者),復制到copy數組中(也從下標0開始) System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength)); return copy; }
7、CopyOnWrite的應用場景
CopyOnWrite並發容器用於讀多寫少的並發場景。
比如白名單,黑名單,商品類目的訪問和更新場景,假如我們有一個搜索網站,用戶在這個網站的搜索框中,輸入關鍵字搜索內容,但是某些關鍵字不允許被搜索。
這些不能被搜索的關鍵字會被放在一個黑名單當中,黑名單每天晚上更新一次。當用戶搜索時,會檢查當前關鍵字在不在黑名單當中,如果在,則提示不能搜索。
8、CopyOnWrite的缺點
CopyOnWrite容器有很多優點(解決開發工作中的多線程的並發問題),但是同時也存在兩個問題,即內存占用問題和數據一致性問題。
1.內存占用問題。
因為CopyOnWrite的寫時復制機制,所以在進行寫操作的時候,內存里會同時駐扎兩個對象的內存,舊的對象和新寫入的對象(注意:在復制的時候只是復制容器里的引用,只是在寫的時候會創建新對象添加到新容器里,而舊容器的對象還在使用,所以有兩份對象內存)。
如果這些對象占用的內存比較大,比如說200M左右,那么再寫入100M數據進去,內存就會占用300M,那么這個時候很有可能造成頻繁的Yong GC和Full GC。
針對內存占用問題,可以通過壓縮容器中的元素的方法來減少大對象的內存消耗,比如,如果元素全是10進制的數字,可以考慮把它壓縮成36進制或64進制。
或者不使用CopyOnWrite容器,而使用其他的並發容器,如ConcurrentHashMap。
2.數據一致性問題。
CopyOnWrite容器只能保證數據的最終一致性,不能保證數據的實時一致性。所以如果你希望寫入的的數據,馬上能讀到,請不要使用CopyOnWrite容器。
9、總結:
- 1.CopyOnWriteArrayList適用於讀多寫少的場景
- 2.在並發操作容器對象時不會拋出ConcurrentModificationException,並且返回的元素與迭代器創建時的元素是一致的
- 3.容器對象的復制需要一定的開銷,如果對象占用內存過大,可能造成頻繁的YoungGC和Full GC
- 4.CopyOnWriteArrayList不能保證數據實時一致性,只能保證最終一致性
- 5.在需要並發操作List對象的時候優先使用CopyOnWriteArrayList
- 6.隨着CopyOnWriteArrayList中元素的增加,CopyOnWriteArrayList的修改代價將越來越昂貴,因此,CopyOnWriteArrayList適用於讀操作遠多於修改操作的並發場景中。
