【轉】CopyOnWriteArrayList

初識CopyOnWriteArrayListjava

第一次見到CopyOnWriteArrayList,是在研究JDBC的時候,每個數據庫的Driver都是維護在一個CopyOnWriteArrayList中的,爲了證實這一點,貼兩段代碼,第一段在com.mysql.jdbc.Driver下,也就是咱們寫Class.forName("...")中的內容:mysql

public class Driver extends NonRegisteringDriver
  implements java.sql.Driver
{
  public Driver()
    throws SQLException
  {
  }

  static
  {
    try
    {
      DriverManager.registerDriver(new Driver());
    } catch (SQLException E) {
      throw new RuntimeException("Can't register driver!");
    }
  }
}

看到com.mysql.jdbc.Driver調用了DriverManager的registerDriver方法,這個類在java.sql.DriverManager下:sql

public class DriverManager
{
  private static final CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList();
  private static volatile int loginTimeout = 0;
  private static volatile PrintWriter logWriter = null;
  private static volatile PrintStream logStream = null;
  private static final Object logSync = new Object();
  static final SQLPermission SET_LOG_PERMISSION = new SQLPermission("setLog");
   ...
}

看到全部的DriverInfo都在CopyOnWriteArrayList中。既然看到了CopyOnWriteArrayList,我天然免不了要研究一番爲何JDK使用的是這個List。數據庫

首先提兩點:數組

一、CopyOnWriteArrayList位於java.util.concurrent包下,可想而知,這個類是爲併發而設計的緩存

二、CopyOnWriteArrayList,顧名思義,Write的時候老是要Copy,也就是說對於CopyOnWriteArrayList,任何可變的操做(add、set、remove等等)都是伴隨複製這個動做的,後面會解讀CopyOnWriteArrayList的底層實現機制安全

四個關注點在CopyOnWriteArrayList上的答案併發

關  注  點 結      論
CopyOnWriteArrayList是否容許空 容許
CopyOnWriteArrayList是否容許重複數據 容許
CopyOnWriteArrayList是否有序 有序
CopyOnWriteArrayList是否線程安全 線程安全

如何向CopyOnWriteArrayList中添加元素dom

對於CopyOnWriteArrayList來講,增長、刪除、修改、插入的原理都是同樣的,因此用增長元素來分析一下CopyOnWriteArrayList的底層實現機制就能夠了。先看一段代碼:分佈式

public static void main(String[] args){
     List<Integer> list = new CopyOnWriteArrayList<Integer>();
     list.add(1);
     list.add(2);
}

看一下這段代碼作了什麼,先是第3行的實例化一個新的CopyOnWriteArrayList:

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    private static final long serialVersionUID = 8673264195747942595L;

    /** The lock protecting all mutators */
    transient final ReentrantLock lock = new ReentrantLock();

    /** The array, accessed only via getArray/setArray. */
    private volatile transient Object[] array;
    ...
}
public CopyOnWriteArrayList() {
    setArray(new Object[0]);
}
final void setArray(Object[] a) {
    array = a;
}

看到,對於CopyOnWriteArrayList來講,底層就是一個Object[] array,而後實例化一個CopyOnWriteArrayList,用圖來表示很是簡單:

就是這樣,Object array指向一個數組大小爲0的數組。接着看一下,第4行的add一個整數1作了什麼,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();
}
}

畫一張圖表示一下:

每一步都清楚地表示在圖上了,一次add大體經歷了幾個步驟:

一、加鎖

二、拿到原數組,獲得新數組的大小(原數組大小+1),實例化出一個新的數組來

三、把原數組的元素複製到新數組中去

四、新數組最後一個位置設置爲待添加的元素(由於新數組的大小是按照原數組大小+1來的)

五、把Object array引用指向新數組

六、解鎖

整個過程看起來比較像ArrayList的擴容。有了這個基礎,咱們再來看一下第5行的add了一個整數2作了什麼,這應該很是簡單了,仍是畫一張圖來表示:

和前面差很少,就不解釋了。

另外,插入、刪除、修改操做也都是同樣,每一次的操做都是以對Object[] array進行一次複製爲基礎的,若是上面的流程看懂了,那麼研究插入、刪除、修改的源代碼應該不難。

普通List的缺陷

經常使用的List有ArrayList、LinkedList、Vector,其中前兩個是線程非安全的,最後一個是線程安全的。我有一種場景,兩個線程操做了同一個List,分別對同一個List進行迭代和刪除,就如同下面的代碼:

public static class T1 extends Thread
{
    private List<Integer> list;
    
    public T1(List<Integer> list)
    {
        this.list = list;
    }
    
    public void run()
    {
        for (Integer i : list)
        {
        }
    }
}
    
public static class T2 extends Thread
{
    private List<Integer> list;
    
    public T2(List<Integer> list)
    {
        this.list = list;
    }
    
    public void run()
    {
        for (int i = 0; i < list.size(); i++)
        {
            list.remove(i);
        }
    }
}

首先我在這兩個線程中放入ArrayList並啓動這兩個線程:

public static void main(String[] args)
{
    List<Integer> list = new ArrayList<Integer>();
    
    for (int i = 0; i < 10000; i++)
    {
        list.add(i);
    }
    
    T1 t1 = new T1(list);
    T2 t2 = new T2(list);
    t1.start();
    t2.start();
}

運行結果爲:

Exception in thread "Thread-0" java.util.ConcurrentModificationException
    at java.util.AbstractList$Itr.checkForComodification(AbstractList.java:372)
    at java.util.AbstractList$Itr.next(AbstractList.java:343)
    at com.xrq.test60.TestMain$T1.run(TestMain.java:19)

把ArrayList換成LinkedList,main函數的代碼就不貼了,運行結果爲:

Exception in thread "Thread-0" java.util.ConcurrentModificationException
    at java.util.LinkedList$ListItr.checkForComodification(LinkedList.java:761)
    at java.util.LinkedList$ListItr.next(LinkedList.java:696)
    at com.xrq.test60.TestMain$T1.run(TestMain.java:19)

可能有人以爲,這兩個線程都是線程非安全的類,因此不行。其實這個問題和線程安不安全沒有關係,換成Vector看一下運行結果:

Exception in thread "Thread-0" java.util.ConcurrentModificationException
    at java.util.AbstractList$Itr.checkForComodification(AbstractList.java:372)
    at java.util.AbstractList$Itr.next(AbstractList.java:343)
    at com.xrq.test60.TestMain$T1.run(TestMain.java:19)

Vector雖然是線程安全的,可是隻是一種相對的線程安全而不是絕對的線程安全,它只可以保證增、刪、改、查的單個操做必定是原子的,不會被打斷,可是若是組合起來用,並不能保證線程安全性。好比就像上面的線程1在遍歷一個Vector中的元素、線程2在刪除一個Vector中的元素同樣,勢必產生併發修改異常,也就是fail-fast

 

CopyOnWriteArrayList的做用

把上面的代碼修改一下,用CopyOnWriteArrayList:

public static void main(String[] args)
{
    List<Integer> list = new CopyOnWriteArrayList<Integer>();
        
    for (int i = 0; i < 10; i++)
    {
        list.add(i);
    }
    
    T1 t1 = new T1(list);
    T2 t2 = new T2(list);
    t1.start();
    t2.start();
}

能夠運行一下這段代碼,是沒有任何問題的。

看到我把元素數量改小了一點,由於咱們從上面的分析中應該能夠看出,CopyOnWriteArrayList的缺點,就是修改代價十分昂貴,每次修改都伴隨着一次的數組複製;但同時優勢也十分明顯,就是在併發下不會產生任何的線程安全問題,也就是絕對的線程安全,這也是爲何咱們要使用CopyOnWriteArrayList的緣由。

另外,有兩點必須講一下。我認爲CopyOnWriteArrayList這個併發組件,其實反映的是兩個十分重要的分佈式理念:

(1)讀寫分離

咱們讀取CopyOnWriteArrayList的時候讀取的是CopyOnWriteArrayList中的Object[] array,可是修改的時候,操做的是一個新的Object[] array,讀和寫操做的不是同一個對象,這就是讀寫分離。這種技術數據庫用的很是多,在高併發下爲了緩解數據庫的壓力,即便作了緩存也要對數據庫作讀寫分離,讀的時候使用讀庫,寫的時候使用寫庫,而後讀庫、寫庫之間進行必定的同步,這樣就避免同一個庫上讀、寫的IO操做太多

(2)最終一致

對CopyOnWriteArrayList來講,線程1讀取集合裏面的數據,未必是最新的數據。由於線程二、線程三、線程4四個線程都修改了CopyOnWriteArrayList裏面的數據,可是線程1拿到的仍是最老的那個Object[] array,新添加進去的數據並無,因此線程1讀取的內容未必準確。不過這些數據雖然對於線程1是不一致的,可是對於以後的線程必定是一致的,它們拿到的Object[] array必定是三個線程都操做完畢以後的Object array[],這就是最終一致。最終一致對於分佈式系統也很是重要,它經過容忍必定時間的數據不一致,提高整個分佈式系統的可用性與分區容錯性。固然,最終一致並非任何場景都適用的,像火車站售票這種系統用戶對於數據的實時性要求很是很是高,就必須作成強一致性的。

最後總結一點,隨着CopyOnWriteArrayList中元素的增長,CopyOnWriteArrayList的修改代價將愈來愈昂貴,所以,CopyOnWriteArrayList適用於讀操做遠多於修改操做的併發場景中

相關文章
相關標籤/搜索