CopyOnWriteArrayList簡介
CopyOnWriteArrayList容器是Collections.synchronizedList(List list)的替代方案,CopyOnWriteArrayList在某些情況下具有更好的效能,考慮讀遠大於寫的情境,如果把所有的讀操作進行加鎖,因為只有一個讀線程能夠獲得鎖,所以其他的讀線程都必須等待,大大影響效能。CopyOnWriteArrayList稱為“寫時複製”容器,就是在多線程操作容器物件時,把容器複製一份,這樣線上程內部的修改就與其他線程無關了,而且這樣設計可以做到不阻塞其他的讀線程。從JDK1.5開始Java並發包裡提供了兩個使用CopyOnWrite機制實現的並發容器,它們是CopyOnWriteArrayList和CopyOnWriteArraySet。
CopyOnWriteArrayList容器使用樣本
下面的代碼示範如何使用CopyOnWriteArrayList容器:
package com.rhwayfun.patchwork.concurrency.r0408;import java.text.DateFormat;import java.text.SimpleDateFormat;import java.util.Date;import java.util.List;import java.util.concurrent.CopyOnWriteArrayList;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.atomic.AtomicLong;/** * Created by rhwayfun on 16-4-8. */public class CopyOnWriteArrayListDdemo { /** * 內容編號 */ private static AtomicLong contentNum; /** * 日期格式器 */ private static DateFormat format; /** * 線程池 */ private final ExecutorService threadPool; public CopyOnWriteArrayListDdemo() { contentNum = new AtomicLong(); format = new SimpleDateFormat("HH:mm:ss"); threadPool = Executors.newFixedThreadPool(10); } public void doExec(int num) throws InterruptedException { List<String> list = new CopyOnWriteArrayList<>(); for (int i = 0; i < num; i++){ list.add(i,"main-content-" + i); } //5個寫線程 for (int i = 0; i < 5; i++){ threadPool.execute(new Writer(list,i)); } //啟動10個讀線程 for (int i = 0; i < 10; i++){ threadPool.execute(new Reader(list)); } //關閉線程池 threadPool.shutdown(); } /** * 寫線程 * * @author rhwayfun */ static class Writer implements Runnable { private final List<String> copyOnWriteArrayList; private int i; public Writer(List<String> copyOnWriteArrayList,int i) { this.copyOnWriteArrayList = copyOnWriteArrayList; this.i = i; } @Override public void run() { copyOnWriteArrayList.add(i,"content-" + contentNum.incrementAndGet()); System.out.println(Thread.currentThread().getName() + ": write content-" + contentNum.get() + " " +format.format(new Date())); System.out.println(Thread.currentThread().getName() + ": remove " + copyOnWriteArrayList.remove(i)); } } static class Reader implements Runnable { private final List<String> list; public Reader(List<String> list) { this.list = list; } @Override public void run() { for (String s : list) { System.out.println(Thread.currentThread().getName() + ": read " + s + " " +format.format(new Date())); } } } public static void main(String[] args) throws InterruptedException { CopyOnWriteArrayListDdemo demo = new CopyOnWriteArrayListDdemo(); demo.doExec(5); }}
首先啟動5個寫線程,再啟動10個讀線程,運行該程式發現並沒有出現異常,所以使用寫時複製容器效率很高。代碼的運行結果如下:
CopyOnWriteArrayList源碼剖析
先說說CopyOnWriteArrayList容器的實現原理:簡單地說,就是在需要對容器進行操作的時候,將容器拷貝一份,對容器的修改等操作都在容器的拷貝中進行,當操作結束,再把容器容器的拷貝指向原來的容器。這樣設計的好處是實現了讀寫分離,並且讀讀不會發生阻塞。下面的源碼是CopyOnWriteArrayList的add方法實現:
public void add(int index, E element) { final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; if (index > len || index < 0) throw new IndexOutOfBoundsException("Index: "+index+ ", Size: "+len); Object[] newElements; int numMoved = len - index; //1、複製出一個新的數組 if (numMoved == 0) newElements = Arrays.copyOf(elements, len + 1); else { newElements = new Object[len + 1]; System.arraycopy(elements, 0, newElements, 0, index); System.arraycopy(elements, index, newElements, index + 1, numMoved); } //2、把新元素添加到新數組中 newElements[index] = element; //3、把數組指向原來的數組 setArray(newElements); } finally { lock.unlock(); } }
上面的三個步驟實現了寫時複製的思想,在讀資料的時候不會鎖住list,因為寫操作是在原來容器的拷貝上進行的。而且,可以看到,如果對容器拷貝修改的過程中又有新的讀線程進來,那麼讀到的還是舊的資料。讀的代碼如下:
public E get(int index) { return get(getArray(), index); } final Object[] getArray() { return array; }
CopyOnWrite並發容器用於讀多寫少的並發情境。比如白名單,黑名單,商品類目的訪問和更新情境。
CopyOnWriteArrayList的缺點
從CopyOnWriteArrayList的實現原理可以看到因為在需要容器物件的時候進行拷貝,所以存在兩個問題:記憶體佔用問題和資料一致性問題。
記憶體佔用問題:因為需要將原來的對象進行拷貝,這需要一定的開銷。特別是當容器物件過大的時候,因為拷貝而佔用的記憶體將增加一倍(原來駐留在記憶體的對象仍然在使用,拷貝之後就有兩份對象在記憶體中,所以增加了一倍記憶體)。而且,在高並發的情境下,因為每個線程都拷貝一份對象在記憶體中,這種情況體現得更明顯。由於JVM的最佳化機制,將會觸發頻繁的Young GC和Full GC,從而使整個系統的效能下降。
資料一致性問題:CopyOnWriteArrayList不能保證即時一致性,因為讀線程在將引用重新指向原來的對象之前再次讀到的資料是舊的,所以CopyOnWriteArrayList只能保證最終一致性。因此在需要即時一致性的廠幾個CopyOnWriteArrayList是不能使用的。
CopyOnWriteArrayList小結: CopyOnWriteArrayList適用於讀多寫少的情境 在並行作業容器物件時不會拋出ConcurrentModificationException,並且返回的元素與迭代器建立時的元素是一致的 容器物件的複製需要一定的開銷,如果對象佔用記憶體過大,可能造成頻繁的YoungGC和Full GC CopyOnWriteArrayList不能保證資料即時一致性,只能保證最終一致性 在需要並行作業List對象的時候優先使用CopyOnWriteArrayList