Java 經典面試題:聊一聊 JUC 下的 CopyOnWriteArrayList

ArrayList 是咱們經常使用的工具類之一,可是在多線程的狀況下,ArrayList 做爲共享變量時,並非線程安全的。主要有如下兩個緣由:java

  • 一、 ArrayList 自身的 elementData、size、modCount 在進行操做的時候,都沒有加鎖;
  • 二、這些變量沒有被 volatile 修飾,在多線程的狀況下,對這些變量操做可能會出現值被覆蓋的狀況;

若是咱們想在多線程狀況下使用 ArrayList 怎麼辦?有如下幾種辦法:數組

  • 使用 Collections.SynchronizedList ;
  • 使用 JUC 下的 CopyOnWriteArrayList;

先來看看 SynchronizedLis,Collections 其實就是對 ArrayList 進行了一個加鎖包裝,這個從源碼中能夠看出;安全

...部分源碼,完整源碼請查看 JDK 源碼...
public void add(int index, E element) {
    synchronized (mutex) {list.add(index, element);}
}
public E remove(int index) {
    synchronized (mutex) {return list.remove(index);}
}
複製代碼

對於 Collections.SynchronizedList 比較簡單,就是鎖包裝了一下,就很少說了~多線程

CopyOnWriteArrayList 也是 JUC 下面的一個併發容器類。不知道你發現沒有,但凡你經常使用的集合類,在 JUC 下基本上均可以找到一個併發類,好比 hashMap 有對應的 ConcurrentHashMap。架構

CopyOnWriteArrayList 跟 ArrayList 在總體架構上並無什麼區別,底層都是基於數組實現的。不一樣的地方大概有兩點:併發

  • 底層數組被 volatile 關鍵字修飾;
  • 對數組進行數據變動時加鎖;

CopyOnWriteArrayList 的加鎖操做跟 Collections.SynchronizedList 簡單的加鎖還不同,CopyOnWriteArrayList 中的加鎖過程仍是很是值得學習的。CopyOnWriteArrayList 的加鎖過程,大概能夠歸納爲如下四步:app

  • 一、加鎖;
  • 二、從原數組中拷貝出新數組;
  • 三、在新數組上進行操做,並把新數組賦值給數組容器;
  • 四、解鎖;

結合源碼來深刻了解 CopyOnWriteArrayList 的併發實現,咱們選擇 ArrayList 最簡單的將元素新增數組尾部的操做來分析實現過程,源碼以下:工具

/** * 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();
    }
}
複製代碼

CopyOnWriteArrayList 就是經過加鎖來講實現容器安全的,可能你會有疑問,爲何引入一個新數組,數組的拷貝仍是消耗時間的,直接在原數組上操做不就行了嗎?。主要緣由有如下兩點:學習

  • volatile 關鍵字修飾的是數組,若是咱們簡單的在原來數組上修改其中某幾個元素的值,是沒法觸發可見性的,咱們必須經過修改數組的內存地址才行,也就說要對數組進行從新賦值才行。
  • 在新的數組上進行拷貝,對老數組沒有任何影響,只有新數組徹底拷貝完成以後,外部才能訪問到,下降了在賦值過程當中,老數組數據變更的影響。好比經典的 ConcurrentModificationException 異常問題。

其餘的新增方法就本身去查看源碼了,相差很少,基本上是同樣的。對數組的刪除跟新增都是差很少,不一樣的地方是在刪除了時候,賦值給新數組時會出現不一樣的選擇策略。我把源碼貼上:this

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();
    }
}
複製代碼

CopyOnWriteArrayList 還有其餘的方法,在這裏我就不過多介紹了。根據大家本身的疑問去扒一扒 CopyOnWriteArrayList 的源碼就知道了,整體來講 CopyOnWriteArrayList 並不難,甚至感受比 ArrayList 要簡單。

總結一下:CopyOnWriteArrayList 是安全的併發容器,有如下兩個特色:

  • 一、對數組的寫操做加鎖,讀操做不加鎖;
  • 二、經過加鎖 + 數組拷貝+ volatile 來保證線程安全;

歡迎關注公衆號【互聯網平頭哥】,一塊兒成長,一塊兒進步~。

互聯網平頭哥
相關文章
相關標籤/搜索