面試必問之 CopyOnWriteArrayList,你瞭解多少?

一、摘要

在介紹 CopyOnWriteArrayList 之前,我們一起先來看看如下方法執行結果,代碼內容如下:

public static void main(String[] args) {
    List<String> list = new ArrayList<String>();
    list.add("1");
    list.add("2");
    list.add("1");
    System.out.println("原始list元素:"+ list.toString());
    //通過對象移除等於內容爲1的元素
    for (String item : list) {
        if("1".equals(item)) {
            list.remove(item);
        }
    }
    System.out.println("通過對象移除後的list元素:"+ list.toString());
}

執行結果內容如下:

原始list元素:[1, 2, 1]
Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
	at java.util.ArrayList$Itr.next(ArrayList.java:859)
	at com.example.container.a.TestList.main(TestList.java:16)

很遺憾,結果並沒有達到我們想要的預期效果,執行之後直接報錯!拋ConcurrentModificationException異常!

爲啥會拋這個異常呢?

我們一起來看看,foreach 寫法實際上是對List.iterator() 迭代器的一種簡寫,因此我們可以從分析List.iterator() 迭代器進行入手,看看爲啥會拋這個異常。

ArrayList類中的Iterator迭代器實現,源碼內容:

通過代碼我們發現 ItrArrayList 中定義的一個私有內部類,每次調用nextremove方法時,都會調用checkForComodification方法,源碼如下:

/**修改次數檢查*/
final void checkForComodification() {
	//檢查List中的修改次數是否與迭代器類中的修改次數相等
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

checkForComodification方法,實際上是用來檢查List中的修改次數modCount是否與迭代器類中的修改次數expectedModCount相等,如果不相等,就會拋出ConcurrentModificationException異常!

那麼問題基本上已經清晰了,上面的運行結果之所以會拋出這個異常,就是因爲List中的修改次數modCount與迭代器類中的修改次數expectedModCount不相同造成的!

閱讀過集合源碼的朋友,可能想起Vector這個類,它不是 JDK 中 ArrayList 線程安全的一個版本麼?

好的,爲了眼見爲實,我們把ArrayList換成Vector來測試一下,代碼如下:

public static void main(String[] args) {
    Vector<String> list = new Vector<String>();
    //模擬10個線程向list中添加內容,並且讀取內容
    for (int i = 0; i < 5; i++) {
        final int j = i;
        new Thread(new Runnable() {
            @Override
            public void run() {
                //添加內容
                list.add(j + "-j");

                //讀取內容
                for (String str : list) {
                    System.out.println("內容:" + str);
                }
            }
        }).start();
    }
}

執行程序,運行結果如下:

還是一樣的結果,拋異常了Vector雖然線程安全,只不過是加了synchronized關鍵字,但是迭代問題完全沒有解決!

繼續回到本文要介紹的 CopyOnWriteArrayList 類,我們把上面的例子,換成CopyOnWriteArrayList類來試試,源碼內容如下:

public static void main(String[] args) {
    //將ArrayList換成CopyOnWriteArrayList
    CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
    list.add("1");
    list.add("2");
    list.add("1");
    System.out.println("原始list元素:"+ list.toString());

    //通過對象移除等於11的元素
    for (String item : list) {
        if("1".equals(item)) {
            list.remove(item);
        }
    }
    System.out.println("通過對象移除後的list元素:"+ list.toString());
}

執行結果如下:

原始list元素:[1, 2, 1]
通過對象移除後的list元素:[2]

呃呵,執行成功了,沒有報錯!是不是很神奇~~

當然,類似上面這樣的例子有很多,比如寫10個線程向list中添加元素讀取內容,也會拋出上面那個異常,操作如下:

public static void main(String[] args) {
    final List<String> list = new ArrayList<>();
    //模擬10個線程向list中添加內容,並且讀取內容
    for (int i = 0; i < 10; i++) {
        final int j = i;
        new Thread(new Runnable() {
            @Override
            public void run() {
                //添加內容
                list.add(j + "-j");

                //讀取內容
                for (String str : list) {
                    System.out.println("內容:" + str);
                }
            }
        }).start();
    }
}

類似的操作例子就非常多了,這裏就不一一舉例了。

CopyOnWriteArrayList 實際上是 ArrayList 一個線程安全的操作類!

從它的名字可以看出,CopyOnWrite 是在寫入的時候,不修改原內容,而是將原來的內容複製一份到新的數組,然後向新數組寫完數據之後,再移動內存指針,將目標指向最新的位置。

二、簡介

從 JDK1.5 開始 Java 併發包裏提供了兩個使用CopyOnWrite 機制實現的併發容器,分別是CopyOnWriteArrayListCopyOnWriteArraySet

從名字上看,CopyOnWriteArrayList主要針對動態數組,一個線程安全版本的 ArrayList !

CopyOnWriteArraySet主要針對集,CopyOnWriteArraySet可以理解爲HashSet線程安全的操作類,我們都知道HashSet基於散列表HashMap實現,但是CopyOnWriteArraySet並不是基於散列表實現,而是基於CopyOnWriteArrayList動態數組實現!

關於這一點,我們可以從它的源碼中得出結論,部分源碼內容:

從源碼上可以看出,CopyOnWriteArraySet默認初始化的時候,實例化了CopyOnWriteArrayList類,CopyOnWriteArraySet的大部分方法,例如addremove等方法都基於CopyOnWriteArraySet實現!

兩者最大的不同點是,CopyOnWriteArrayList可以允許元素重複,而CopyOnWriteArraySet不允許有重複的元素!

好了,繼續來 BB 本文要介紹的CopyOnWriteArrayList類~~

打開CopyOnWriteArrayList類的源碼,內容如下:

可以看到 CopyOnWriteArrayList 的存儲元素的數組array變量,使用了volatile關鍵字保證的多線程下數據可見行;同時,使用了ReentrantLock可重入鎖對象,保證線程操作安全。

在初始化階段,CopyOnWriteArrayList默認給數組初始化了一個對象,當然,初始化方法還有很多,比如如下我們經常會用到的一個初始化方法,源碼內容如下:

這個方法,表示如果我們傳入的是一個 ArrayList數組對象,會將對象內容複製一份到新的數組中,然後初始化進去,操作如下:

List<String> list = new ArrayList<>();
...
//CopyOnWriteArrayList將list內容複製出來,並創建一個新的數組
CopyOnWriteArrayList<String> copyList = new CopyOnWriteArrayList<>(list);

CopyOnWriteArrayList是對原數組內容進行復制再寫入,那麼是不是也存在多線程下操作也會發生衝突呢?

下面我們再一起來看看它的方法實現!

三、常用方法

3.1、添加元素

add()方法是CopyOnWriteArrayList的添加元素的入口!

CopyOnWriteArrayList之所以能保證多線程下安全操作, add()方法功不可沒,源碼如下:

操作步驟如下:

  • 1、獲得對象鎖;
  • 2、獲取數組內容;
  • 3、將原數組內容複製到新數組;
  • 4、寫入數據;
  • 5、將array數組變量地址指向新數組;
  • 6、釋放對象鎖;

在 Java 中,獨佔鎖方面,有2種方式可以保證線程操作安全,一種是使用虛擬機提供的synchronized 來保證併發安全,另一種是使用JUC包下的ReentrantLock可重入鎖來保證線程操作安全。

CopyOnWriteArrayList使用了ReentrantLock這種可重入鎖,保證了線程操作安全,同時數組變量array使用volatile保證多線程下數據的可見行!

其他的,還有指定下標進行添加的方法,如add(int index, E element),操作類似,先找到需要添加的位置,如果是中間位置,則以添加位置爲分界點,分兩次進行復制,最後寫入數據!

3.2、移除元素

remove()方法是CopyOnWriteArrayList的移除元素的入口!

源碼如下:

操作類似添加方法,步驟如下:

  • 1、獲得對象鎖;
  • 2、獲取數組內容;
  • 3、判斷移除的元素是否爲數組最後的元素,如果是最後的元素,直接將舊元素內容複製到新數組,並重新設置array值;
  • 4、如果是中間元素,以index爲分界點,分兩節複製;
  • 5、將array數組變量地址指向新數組;
  • 6、釋放對象鎖;

當然,移除的方法還有基於對象的remove(Object o),原理也是一樣的,先找到元素的下標,然後執行移除操作。

3.3、查詢元素

get()方法是CopyOnWriteArrayList的查詢元素的入口!

源碼如下:

public E get(int index) {
    //獲取數組內容,通過下標直接獲取
    return get(getArray(), index);
}

查詢因爲不涉及到數據操作,所以無需使用鎖進行處理!

3.4、遍歷元素

上文中我們介紹到,基本都是在遍歷元素的時候因爲修改次數與迭代器中的修改次數不一致,導致檢查的時候拋異常,我們一起來看看CopyOnWriteArrayList迭代器實現。

打開源碼,可以得出CopyOnWriteArrayList返回的迭代器是COWIterator,源碼如下:

public Iterator<E> iterator() {
    return new COWIterator<E>(getArray(), 0);
}

打開COWIterator類,其實它是CopyOnWriteArrayList的一個靜態內部類,源碼如下:

可以看出,在使用迭代器的時候,遍歷的元素都來自於上面的getArray()方法傳入的對象數組,也就是傳遞進來的 array 數組!

由此可見,CopyOnWriteArrayList 在使用迭代器遍歷的時候,操作的都是原數組,沒有像上面那樣進行修改次數判斷,所以不會拋異常!

當然,從源碼上也可以得出,使用CopyOnWriteArrayList的迭代器進行遍歷元素的時候,不能調用remove()方法移除元素,因爲不支持此操作!

如果想要移除元素,只能使用CopyOnWriteArrayList提供的remove()方法,而不是迭代器的remove()方法,這個需要注意一下!

四、總結

CopyOnWriteArrayList是一個典型的讀寫分離的動態數組操作類!

在寫入數據的時候,將舊數組內容複製一份出來,然後向新的數組寫入數據,最後將新的數組內存地址返回給數組變量;移除操作也類似,只是方式是移除元素而不是添加元素;而查詢方法,因爲不涉及線程操作,所以並沒有加鎖出來!

因爲CopyOnWriteArrayList讀取內容沒有加鎖,在寫入數據的時候同時也可以進行讀取數據操作,因此性能得到很大的提升,但是也有缺陷,對於邊讀邊寫的情況,不一定能實時的讀到最新的數據,比如如下操作:

public static void main(String[] args) throws InterruptedException {
    final CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
    list.add("a");
    list.add("b");
    for (int i = 0; i < 5; i++) {
        final int j =i;
        new Thread(new Runnable() {
            @Override
            public void run() {
                //寫入數據
                list.add("i-" + j);
                //讀取數據
                for (String str : list) {
                    System.out.println("線程-" + Thread.currentThread().getName() + ",讀取內容:" + str);
                }
            }
        }).start();
    }
}

新建5個線程向list中添加元素,執行結果如下:

可以看到,5個線程的讀取內容有差異!

因此CopyOnWriteArrayList很適合讀多寫少的應用場景!

五、參考

1、JDK1.7&JDK1.8 源碼

2、掘金 - 擁抱心中的夢想 - 說一說Java中的CopyOnWriteArrayList

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章