并发容器CopyOnWriteArrayList
2019獨角獸企業(yè)重金招聘Python工程師標準>>>
Copy-On-Write簡稱COW,是一種用于程序設(shè)計中的優(yōu)化策略。其基本思路是,從一開始大家都在共享同一個內(nèi)容,當某個人想要修改這個內(nèi)容的時候,才會真正把內(nèi)容Copy出去形成一個新的內(nèi)容然后再改,這是一種延時懶惰策略。從JDK1.5開始Java并發(fā)包里提供了兩個使用CopyOnWrite機制實現(xiàn)的并發(fā)容器,它們是CopyOnWriteArrayList和CopyOnWriteArraySet。CopyOnWrite容器非常有用,可以在非常多的并發(fā)場景中使用到。
CopyOnWrite容器即寫時復(fù)制的容器。通俗的理解是當我們往一個容器添加元素的時候,不直接往當前容器添加,而是先將當前容器進行Copy,復(fù)制出一個新的容器,然后新的容器里添加元素,添加完元素之后,再將原容器的引用指向新的容器。這樣做的好處是我們可以對CopyOnWrite容器進行并發(fā)的讀,而不需要加鎖,因為當前容器不會添加任何元素。所以CopyOnWrite容器也是一種讀寫分離的思想,讀和寫不同的容器。
CopyOnWriteArrayList相當于線程安全的ArrayList,通過增加寫時復(fù)制語義來實現(xiàn)線程安全性。底層也是通過一個可變數(shù)組來實現(xiàn)的。但是和ArrayList不同的時,它具有以下特性:
在使用CopyOnWriteArrayList之前,我們先閱讀其源碼了解下它是如何實現(xiàn)的。以下代碼是向CopyOnWriteArrayList中add方法的實現(xiàn)(向CopyOnWriteArrayList里添加元素),可以發(fā)現(xiàn)在添加的時候是需要加鎖的,否則多線程寫的時候會Copy出N個副本出來。
array: 保存了列表中的數(shù)據(jù)
lock: 修改時加鎖,用于保證線程安全
底層數(shù)據(jù)結(jié)構(gòu)依然是數(shù)組,相比較于ArrayList而言,少了一個表示數(shù)組長度的size變量,獲取列表長度是通過下面的方法
首先來看一下構(gòu)造函數(shù),如下所示:
? ? private volatile transient Object[] array;
? ? final Object[] getArray() {
? ? ? ? return array;
? ? }
? ? final void setArray(Object[] a) {
? ? ? ? array = a;
? ? }
? ? // -----開始構(gòu)造函數(shù)----------------------------
? ? 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.
? ? public CopyOnWriteArrayList(E[] toCopyIn) {
? ? ? ? setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
? ? }
使用一個指向volatile類型的Object數(shù)組來保存容器元素。構(gòu)造函數(shù)中都會根據(jù)參數(shù)值重新生成一個新的數(shù)組。
1.添加元素
?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);// 重新生成一個新的數(shù)組實例,并將原始數(shù)組的元素拷貝到新數(shù)組中
? ? ? ? ? ? newElements[len] = e; ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? // 添加新的元素到新數(shù)組的末尾
? ? ? ? ? ? setArray(newElements); ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?// 更新底層數(shù)組
? ? ? ? ? ? return true;
? ? ? ? } finally {
? ? ? ? ? ? lock.unlock();
? ? ? ? }
? ? }
讀的時候不需要加鎖,如果讀的時候有多個線程正在向CopyOnWriteArrayList添加數(shù)據(jù),讀還是會讀到舊的數(shù)據(jù),因為寫的時候不會鎖住舊的CopyOnWriteArrayList。
ArrayList新增元素時,可能導(dǎo)致數(shù)組擴容;CopyOnWriteArrayList在列表的修改時,采用數(shù)組拷貝,在新的數(shù)組上進行操作,從這點出發(fā),應(yīng)該不存在擴容的問題,因為每次修改都會導(dǎo)致數(shù)組的重新拷貝
有兩點必須清楚:
?第一,在”添加操作“開始前,獲取獨占鎖(lock),若此時有需要線程要獲取鎖,則必須等待;在操作完畢后,釋放獨占鎖(lock),此時其它線程才能獲取鎖。通過獨占鎖,來防止多線程同時修改數(shù)據(jù)!
?第二,操作完畢時,會通過setArray()來更新volatile數(shù)組。對一個volatile變量的讀,總是能看到(任意線程)對這個volatile變量最后的寫入;這樣,每次添加元素之后,其它線程都能看到新添加的元素。
2.獲取元素:
public E get(int index) {
? ? return get(getArray(), index);
}
private E get(Object[] a, int index) {
? ? return (E) a[index];
}
將底層volatile數(shù)組指定索引處的元素返回即可。
3.刪除元素:
以remove(int index)為例,來對“CopyOnWriteArrayList的刪除操作”進行說明。下面是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); // 獲取volatile數(shù)組中指定索引處的元素值
? ? ? ? int numMoved = len - index - 1;
? ? ? ? if (numMoved == 0) // 如果被刪除的是最后一個元素,則直接通過Arrays.copyOf()進行處理,而不需要新建數(shù)組
? ? ? ? ? ? setArray(Arrays.copyOf(elements, len - 1));
? ? ? ? else {
? ? ? ? ? ? Object[] newElements = new Object[len - 1];
? ? ? ? ? ? System.arraycopy(elements, 0, newElements, 0, index); ? ?// 拷貝刪除元素前半部分數(shù)據(jù)到新數(shù)組中
? ? ? ? ? ? System.arraycopy(elements, index + 1, newElements, index, numMoved);// 拷貝刪除元素后半部分數(shù)據(jù)到新數(shù)組中
? ? ? ? ? ? setArray(newElements); // 更新volatile數(shù)組
? ? ? ? }
? ? ? ? return oldValue;
? ? } finally {
? ? ? ? lock.unlock();
? ? }
}
從刪除的實現(xiàn),可確定以下幾點:
-
修改加鎖,確保同一時刻只有一個線程對數(shù)組進行修改
-
修改并不是在原數(shù)組上進行的,而是創(chuàng)建一個新的數(shù)組,在新的數(shù)組上進行操作操作,然后將tables引用指向新的數(shù)組
-
修改必然會涉及到數(shù)組內(nèi)容的拷貝
4.遍歷元素:
public Iterator<E> iterator() {
? ? ? ? return new COWIterator<E>(getArray(), 0);
? ? }
? ? public ListIterator<E> listIterator() {
? ? ? ? return new COWIterator<E>(getArray(), 0);
? ? }
? ? public ListIterator<E> listIterator(final int index) {
? ? ? ? Object[] elements = getArray();
? ? ? ? int len = elements.length;
? ? ? ? if (index<0 || index>len)
? ? ? ? ? ? throw new IndexOutOfBoundsException("Index: "+index);
? ? ? ? return new COWIterator<E>(elements, index);
? ? }
? ? private static class COWIterator<E> implements ListIterator<E> {
? ? ? ? private final Object[] snapshot; // 保存數(shù)組的快照,是一個不可變的對象
? ? ? ? private int cursor;
? ? ? ? private COWIterator(Object[] elements, int initialCursor) {
? ? ? ? ? ? cursor = initialCursor;
? ? ? ? ? ? snapshot = elements;
? ? ? ? }
? ? ? ? public boolean hasNext() {
? ? ? ? ? ? return cursor < snapshot.length;
? ? ? ? }
? ? ? ? public boolean hasPrevious() {
? ? ? ? ? ? return cursor > 0;
? ? ? ? }
? ? ? ? @SuppressWarnings("unchecked")
? ? ? ? public E next() {
? ? ? ? ? ? if (! hasNext())
? ? ? ? ? ? ? ? throw new NoSuchElementException();
? ? ? ? ? ? return (E) snapshot[cursor++];
? ? ? ? }
? ? ? ? @SuppressWarnings("unchecked")
? ? ? ? public E previous() {
? ? ? ? ? ? if (! hasPrevious())
? ? ? ? ? ? ? ? throw new NoSuchElementException();
? ? ? ? ? ? return (E) snapshot[--cursor];
? ? ? ? }
? ? ? ? public int nextIndex() {
? ? ? ? ? ? return cursor;
? ? ? ? }
? ? ? ? public int previousIndex() {
? ? ? ? ? ? return cursor-1;
? ? ? ? }
? ? ? ? public void remove() {
? ? ? ? ? ? throw new UnsupportedOperationException();
? ? ? ? }
? ? ? ? public void set(E e) {
? ? ? ? ? ? throw new UnsupportedOperationException();
? ? ? ? }
? ? ? ? public void add(E e) {
? ? ? ? ? ? throw new UnsupportedOperationException();
? ? ? ? }
? ? }
如上容器的迭代器中會保存一個不可變的Object數(shù)組對象,那么在進行遍歷這個對象時就不需要再進一步的同步。在每次修改時,都會創(chuàng)建并重新發(fā)布一個新的窗口副本,從而實現(xiàn)了可變性。如上迭代器代碼中保留了一個指向volatile數(shù)組的引用,由于不會被修改,因此多個線程可以同時對它進行迭代,而不會彼此干擾或與修改容器的線程相互干擾。
與之前的ArrayList實現(xiàn)相比,CopyOnWriteArrayList返回迭代器不會拋出ConcurrentModificationException異常,即它不是fail-fast機制的!
?
JDK中并沒有提供CopyOnWriteMap,我們可以參考CopyOnWriteArrayList來實現(xiàn)一個,基本代碼如下:
import?java.util.Collection;
import?java.util.Map;
import?java.util.Set;
public?class?CopyOnWriteMap<K, V>?implements?Map<K, V>, Cloneable {
????private?volatile?Map<K, V> internalMap;
????public?CopyOnWriteMap() {
????????internalMap =?new?HashMap<K, V>();
????}
????public?V put(K key, V value) {
????????synchronized?(this) {
????????????Map<K, V> newMap =?new?HashMap<K, V>(internalMap);
????????????V val = newMap.put(key, value);
????????????internalMap = newMap;
????????????return?val;
????????}
????}
????public?V get(Object key) {
????????return?internalMap.get(key);
????}
????public?void?putAll(Map<??extends?K, ??extends?V> newData) {
????????synchronized?(this) {
????????????Map<K, V> newMap =?new?HashMap<K, V>(internalMap);
????????????newMap.putAll(newData);
????????????internalMap = newMap;
????????}
????}
}
CopyOnWrite的應(yīng)用場景
CopyOnWrite并發(fā)容器用于讀多寫少的并發(fā)場景。比如白名單,黑名單,商品類目的訪問和更新場景,假如我們有一個搜索網(wǎng)站,用戶在這個網(wǎng)站的搜索框中,輸入關(guān)鍵字搜索內(nèi)容,但是某些關(guān)鍵字不允許被搜索。這些不能被搜索的關(guān)鍵字會被放在一個黑名單當中,黑名單每天晚上更新一次。當用戶搜索時,會檢查當前關(guān)鍵字在不在黑名單當中,如果在,則提示不能搜索。實現(xiàn)代碼如下:
package?com.ifeve.book;
import?java.util.Map;
import?com.ifeve.book.forkjoin.CopyOnWriteMap;
/**
?* 黑名單服務(wù)
?*
?* @author fangtengfei
?*
?*/
public?class?BlackListServiceImpl {
????private?static?CopyOnWriteMap<String, Boolean> blackListMap =?new?CopyOnWriteMap<String, Boolean>(
????????????1000);
????public?static?boolean?isBlackList(String id) {
????????return?blackListMap.get(id) ==?null???false?:?true;
????}
????public?static?void?addBlackList(String id) {
????????blackListMap.put(id, Boolean.TRUE);
????}
????/**
?????* 批量添加黑名單
?????*
?????* @param ids
?????*/
????public?static?void?addBlackList(Map<String,Boolean> ids) {
????????blackListMap.putAll(ids);
????}
}
實現(xiàn)很簡單,只要了解了CopyOnWrite機制,我們可以實現(xiàn)各種CopyOnWrite容器,并且在不同的應(yīng)用場景中使用。
代碼很簡單,但是使用CopyOnWriteMap需要注意兩件事情:
1. 減少擴容開銷。根據(jù)實際需要,初始化CopyOnWriteMap的大小,避免寫時CopyOnWriteMap擴容的開銷。
2. 使用批量添加。因為每次添加,容器每次都會進行復(fù)制,所以減少添加次數(shù),可以減少容器的復(fù)制次數(shù)。如使用上面代碼里的addBlackList方法。
CopyOnWrite的缺點
CopyOnWrite容器有很多優(yōu)點,但是同時也存在兩個問題,即內(nèi)存占用問題和數(shù)據(jù)一致性問題。所以在開發(fā)的時候需要注意一下。
內(nèi)存占用問題。因為CopyOnWrite的寫時復(fù)制機制,所以在進行寫操作的時候,內(nèi)存里會同時駐扎兩個對象的內(nèi)存,舊的對象和新寫入的對象(注意:在復(fù)制的時候只是復(fù)制容器里的引用,只是在寫的時候會創(chuàng)建新對象添加到新容器里,而舊容器的對象還在使用,所以有兩份對象內(nèi)存)。如果這些對象占用的內(nèi)存比較大,比如說200M左右,那么再寫入100M數(shù)據(jù)進去,內(nèi)存就會占用300M,那么這個時候很有可能造成頻繁的Yong GC和Full GC。之前我們系統(tǒng)中使用了一個服務(wù)由于每晚使用CopyOnWrite機制更新大對象,造成了每晚15秒的Full GC,應(yīng)用響應(yīng)時間也隨之變長。
針對內(nèi)存占用問題,可以通過壓縮容器中的元素的方法來減少大對象的內(nèi)存消耗,比如,如果元素全是10進制的數(shù)字,可以考慮把它壓縮成36進制或64進制?;蛘卟皇褂肅opyOnWrite容器,而使用其他的并發(fā)容器,如ConcurrentHashMap。
數(shù)據(jù)一致性問題。CopyOnWrite容器只能保證數(shù)據(jù)的最終一致性,不能保證數(shù)據(jù)的實時一致性。所以如果你希望寫入的的數(shù)據(jù),馬上能讀到,請不要使用CopyOnWrite容器。
多記錄一句:
Collections.synchronizedList & CopyOnWriteArrayList
? ? ? ?CopyOnWriteArrayList和Collections.synchronizedList是實現(xiàn)線程安全的列表的兩種方式。兩種實現(xiàn)方式分別針對不同情況有不同的性能表現(xiàn),其中CopyOnWriteArrayList的寫操作性能較差,而多線程的讀操作性能較好。而Collections.synchronizedList的寫操作性能比CopyOnWriteArrayList在多線程操作的情況下要好很多,而讀操作因為是采用了synchronized關(guān)鍵字的方式,其讀操作性能并不如CopyOnWriteArrayList。因此在不同的應(yīng)用場景下,應(yīng)該選擇不同的多線程安全實現(xiàn)類。
轉(zhuǎn)載于:https://my.oschina.net/u/1054538/blog/1594618
《新程序員》:云原生和全面數(shù)字化實踐50位技術(shù)專家共同創(chuàng)作,文字、視頻、音頻交互閱讀總結(jié)
以上是生活随笔為你收集整理的并发容器CopyOnWriteArrayList的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 5.25. Spring boot wi
- 下一篇: UGUI内核大探究(一)EventSys