CopyOnWriteArrayList 你瞭解多少?

https://mp.weixin.qq.com/s/nvOhOPp9_wQXP6sTcjuqlQ java

相信你們對 ConcurrentHashMap 這個線程安全類很是熟悉,可是若是我想在多線程環境下使用 ArrayList,該怎麼處理呢?阿粉今天來給你揭曉答案!數組

1、摘要

在介紹 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異常!併發

爲啥會拋這個異常呢?ide

咱們一塊兒來看看,foreach寫法其實是對List.iterator() 迭代器的一種簡寫,所以咱們能夠從分析List.iterator() 迭代器進行入手,看看爲啥會拋這個異常。性能

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

image.png

經過代碼咱們發現 Itr  ArrayList 中定義的一個私有內部類,每次調用nextremove方法時,都會調用checkForComodification方法,源碼以下:spa

/**修改次數檢查*/
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();
   }
}

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

image.png

仍是同樣的結果,拋異常了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 是在寫入的時候,不修改原內容,而是將原來的內容複製一份到新的數組,而後向新數組寫完數據以後,再移動內存指針,將目標指向最新的位置。

2、簡介

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

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

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

關於這一點,咱們能夠從它的源碼中得出結論,部分源碼內容:

image.png

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

二者最大的不一樣點是,CopyOnWriteArrayList能夠容許元素重複,而CopyOnWriteArraySet不容許有重複的元素!

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

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

image.png

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

在初始化階段,CopyOnWriteArrayList默認給數組初始化了一個對象,固然,初始化方法還有不少,好比以下咱們常常會用到的一個初始化方法,源碼內容以下:

image.png

這個方法,表示若是咱們傳入的是一個 ArrayList數組對象,會將對象內容複製一份到新的數組中,而後初始化進去,操做以下:

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

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

下面咱們再一塊兒來看看它的方法實現!

3、經常使用方法

3.一、添加元素

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

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

image.png

操做步驟以下:

  • 一、得到對象鎖;
  • 二、獲取數組內容;
  • 三、將原數組內容複製到新數組;
  • 四、寫入數據;
  • 五、將array數組變量地址指向新數組;
  • 六、釋放對象鎖;

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

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

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

3.二、移除元素

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

源碼以下:

image.png

操做相似添加方法,步驟以下:

  • 一、得到對象鎖;
  • 二、獲取數組內容;
  • 三、判斷移除的元素是否爲數組最後的元素,若是是最後的元素,直接將舊元素內容複製到新數組,並從新設置array值;
  • 四、若是是中間元素,以index爲分界點,分兩節複製;
  • 五、將array數組變量地址指向新數組;
  • 六、釋放對象鎖;

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

3.三、查詢元素

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

源碼以下:

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

查詢由於不涉及到數據操做,因此無需使用鎖進行處理!

3.四、遍歷元素

上文中咱們介紹到,基本都是在遍歷元素的時候由於修改次數與迭代器中的修改次數不一致,致使檢查的時候拋異常,咱們一塊兒來看看CopyOnWriteArrayList迭代器實現。

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

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

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

image.png

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

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

固然,從源碼上也能夠得出,使用CopyOnWriteArrayList的迭代器進行遍歷元素的時候,不能調用remove()方法移除元素,由於不支持此操做!

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

4、總結

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中添加元素,執行結果以下:

image.png

能夠看到,5個線程的讀取內容有差別!

所以CopyOnWriteArrayList很適合讀多寫少的應用場景!

5、參考

一、JDK1.7&JDK1.8 源碼

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

相關文章
相關標籤/搜索