CopyOnWriteArrayList的原理和使用方法

https://blog.csdn.net/hua631150873/article/details/51306021

CopyOnWriteArrayList:CopyOnWriteArrayList這是一個ArrayList的線程安全的變體,其原理大概可以通俗的理解爲:初始化的時候只有一個容器,很常一段時間,這個容器數據、數量等沒有發生變化的時候,大家(多個線程),都是讀取(假設這段時間裏只發生讀取的操作)同一個容器中的數據,所以這樣大家讀到的數據都是唯一、一致、安全的,但是後來有人往裏面增加了一個數據,這個時候CopyOnWriteArrayList 底層實現添加的原理是先copy出一個容器(可以簡稱副本),再往新的容器裏添加這個新的數據,最後把新的容器的引用地址賦值給了之前那個舊的的容器地址,但是在添加這個數據的期間,其他線程如果要去讀取數據,仍然是讀取到舊的容器裏的數據。

併發示例:

package com.base.java.test;
import java.util.ArrayList;
public class ListConcurrentTest{
    private static final int THREAD_POOL_MAX_NUM = 10;
    private List<String> mList = new ArrayList<String>();
    public static void main(String args[]){
            new ListConcurrentTest().start();
    }
    private void initData() {
        for(int i = 0 ; i <= THREAD_POOL_MAX_NUM ; i ++){
            this.mList.add("...... Line "+(i+1)+" ......");
        }
    }
    private void start(){
      initData();
     ExecutorService service = Executors.newFixedThreadPool(THREAD_POOL_MAX_NUM);
        for(int i = 0 ; i < THREAD_POOL_MAX_NUM ; i ++){
            service.execute(new ListReader(this.mList));
            service.execute(new ListWriter(this.mList,i));
        }
        service.shutdown();
    }
    private class ListReader implements Runnable{
        private List<String> mList ;
        public  ListReader(List<String> list) { 
            this.mList = list;
        }
        @Override
        public void run() {
             if(this.mList!=null){
                for(String str : this.mList){
                 System.out.println(Thread.currentThread().getName()+" : "+ str);
                }
             }
        }
    }
    private class ListWriter implements Runnable{
        private List<String> mList ;
        private int mIndex;
        public  ListWriter(List<String> list,int index) { 
            this.mList = list;
            this.mIndex = index;
        }
        @Override
        public void run() {
            if(this.mList!=null){
                    //this.mList.remove(this.mIndex);
                     this.mList.add("...... add "+mIndex +" ......");
             }
        }
    }
}

上面的代碼毋庸置疑會發生併發異常,直接運行看看效果:

所以目前最大的問題,在同一時間多個線程無法對同一個List進行讀取和增刪,否則就會拋出併發異常。

 

OK,既然出現了問題,那我們直接將ArrayList改成我們今天的主角,CopyOnWriteArrayList,再來進行測試,發現一點問題沒有,運行正常。

所以我們不難發現CopyOnWriteArrayList完美解決了併發的問題。

 

原理:

現在我們知道怎麼用了,那我們就來看看源代碼,到底內部它是怎麼運作的呢?

從上面的圖可以看得出,無論我們用哪一個構造方法創建一個CopyOnWriteArrayList對象,

都會創建一個Object類型的數組,然後賦值給成員array。

提示: transient關鍵字主要啓用的作用是當這個對象要被序列化的時候,不要將被transient聲明的變量(Object[] array)序列化到本地。

 

要看CopyOnWriteArrayList怎麼處理併發的問題,當然要去了解它的增、刪、修改、讀取方法是怎麼處理的了。現在我們直接來看看:


final ReentrantLock lock = this.lock;

lock.lock();

首先使用上面的兩行代碼加上了鎖,保證同一時間只能有一個線程在添加元素。

然後使用Arrays.copyOf(...)方法複製出另一個新的數組,而且新的數組的長度比原來數組的長度+1,副本複製完畢,新添加的元素也賦值添加完畢,最後又把新的副本數組賦值給了舊的數組,最後在finally語句塊中將鎖釋放。

然後我們再來看一個remove,刪除元素,很簡單,就是判斷要刪除的元素是否最後一個,如果最後一個直接在複製副本數組的時候,複製長度爲舊數組的length-1即可;但是如果不是最後一個元素,就先複製舊的數組的index前面元素到新數組中,然後再複製舊數組中index後面的元素到數組中,最後再把新數組複製給舊數組的引用。

最後在finally語句塊中將鎖釋放。

其他的一些重載的增刪、修改方法其實都是一樣的邏輯,這裏就不重複講解了。

最後我們再來看一個讀取操作的方法:

所以我們可以看到,其實讀取的時候是沒有加鎖的。

最後我們再來看一下CopyOnWriteArrayList的優點和缺點:

優點:

1.解決的開發工作中的多線程的併發問題。

缺點:

1.內存佔有問題:很明顯,兩個數組同時駐紮在內存中,如果實際應用中,數據比較多,而且比較大的情況下,佔用內存會比較大,針對這個其實可以用ConcurrentHashMap來代替。

2.數據一致性:CopyOnWrite容器只能保證數據的最終一致性,不能保證數據的實時一致性。所以如果你希望寫入的的數據,馬上能讀到,請不要使用CopyOnWrite容器

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