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容器