1. 程式人生 > >java併發之 CopyOnWriteArrayList的原理和使用方法

java併發之 CopyOnWriteArrayList的原理和使用方法

描述

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物件,
都會建立一個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容器