CopyOnWriteArrayList
實作arraylist多執行緒資料安全的方式
- jdk提供的Collections.SynchronizedList() 所有方法進行添加synchronized塊
public void add(int index, E element) {
synchronized (mutex) {list.add(index, element);}
}
- 使用reentrantLock自己對add時,進行對讀寫操作加鎖
List<String> list = new ArrayList<String>();
ReentrantLock lock = new ReentrantLock();
lock.lock();
list.add("");
lock.unlock();
-
使用讀寫鎖對讀多寫少的場景進行優化
List<String> list = new ArrayList<String>();
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
lock.writeLock().lock();
list.add("");
lock.writeLock().unlock();
lock.readLock().lock();
list.get(1);
lock.readLock().unlock();
上面的各種加鎖方式,或多或少效率上都有問題,效率上最好的讀寫鎖,在大量寫操作的時候,讀操作也會被阻塞,沒法進行讀取資料,在業務上有時候,并不需要實時資料,需要的是快取起來沒有修改前的資料,因此引出了CopyOnWriteArrayList 這個類 可以達到在寫操作時,可以讀取到資料,并不阻塞起來
概念
CopyOnWriteArrayList 底層實作添加的原理是先copy出一個容器,再往新的容器里添加這個新的資料,最后把新的容器的參考地址賦值給了之前那個舊的的容器地址,但是在添加這個資料的期間,其他執行緒如果要去讀取資料,仍然是讀取到舊的容器里的資料,
它的優點:
-
并發資料安全,在少量寫的操作下,進行讀操作,并不阻塞
缺點:?????
-
多了記憶體占用: 寫資料是copy一份完整的資料,單獨進行操作,占用雙份記憶體, 這一點很重要,在現今多執行緒狀態下,如果資料量相當大,但你又使用了該容器,就會導致你堆記憶體存到副本得資料量超級大,在不確定的情況下,有可能會導致outofmemoryerror,這就有問題了;這個問題可以使用分布式快取來存副本,降低單機記憶體帶來得問題,
-
資料一致性: 資料寫完之后,其他執行緒不一定是馬上讀取到最新內容,也就是事務中得幻讀,
CopyOnWriteArrayList<String> list2 = new CopyOnWriteArrayList<>();
list2.add("1");
list2.add("2");
list2.add("3");
list2.add("4");
list2.add("5");
list2.add("6");
list2.get(3);
list2.remove(1);
int j = 0;
for (String str : list2){
if (j==0) list2.add("99");
j++;
}
原始碼分析
CopyOnWriteArrayList 創建基礎陣列的新副本,這通常成本太高,但可能效率更高,當遍歷操作的數量遠遠超過其他方法時突變,并且在您不能或不想的情況下非常有用同步遍歷,但需要排除并發執行緒,“快照”樣式的迭代器方法使用參考迭代器運行時陣列的狀態創建了,
繼續分析成員變數來分析
lock 鎖用來在寫操作時,只允許一個執行緒進行操作,保證資料安全性
final transient ReentrantLock lock = new ReentrantLock();
存盤資料的陣列,當然包括 transient,這個屬性在集合中經常使用
private transient volatile Object[] array;
/**
* 將指定的元素追加到此串列的末尾
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
*/
public boolean add(E e) {
final ReentrantLock lock = this.lock; //獲取到鎖
lock.lock();
try {
Object[] elements = getArray();//獲取到原陣列
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1); //復制一個新的資料
newElements[len] = e;//進行操作
setArray(newElements);//并將原陣列參考指向新陣列
return true;
} finally {
lock.unlock();
}
}
這里沒有用動態擴容,原因還是沒有必要而且還是會占用多的記憶體,本來寫操作時會復制元素
remove方法
public E remove(int index) {
final ReentrantLock lock = this.lock; //添加鎖
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
E oldValue = get(elements, index);//這里獲取老的資料
int numMoved = len - index - 1;
if (numMoved == 0)
setArray(Arrays.copyOf(elements, len - 1));
else {//這里對新陣列進行操作
Object[] newElements = new Object[len - 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
setArray(newElements);
}
return oldValue;
} finally {
lock.unlock();
}
}
CopyOnWriteArraySet
而CopyOnWriteArraySet也是基于CopyOnWriteArrayList實作的,在源代碼中可以看見
private final CopyOnWriteArrayList<E> al;
從源代碼中看只有一個屬性就是 CopyOnWriteArrayList,至于怎么保持資料唯一性,
呼叫的addIfAbsent 方法,這個從名稱就能看出來 如果已經存在則覆寫,
public boolean add(E e) {
return al.addIfAbsent(e);
}
以及remove方法都是呼叫的CopyOnWriteArrayList中的方法進行實作,本身只是對這個物件進行持有,
public boolean remove(Object o) {
return al.remove(o);
}
總結
整個CopyOnWriteArrayList解決就是在寫操作時,無法讀取的問題,用于讀多寫少的場景,因為副本的關系,因此在作業中盡量謹慎使用,也可以自己寫一個 讀寫list來保證資料的安全,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/302443.html
標籤:其他
下一篇:安全防護建議
