併發容器-CopyOnWriteArrayList
本文作者:王一飛,叩丁狼高階講師。原創文章,轉載請註明出處。
接上篇講的併發中的list, 今天來聊聊CopyOnWriteArrayList。
CopyOnWriteArrayList 類是JDK1.5引入的處理併發操作的容器類,他是Arraylist類的一種執行緒安全的變種,在併發環境下, 保證集合的讀與寫安全。
CopyOnWriteArrayList類是執行緒安全的容器,但它的安全是有一定的限制的。他的執行緒安全操作針對是併發環境下執行緒讀與寫安全。簡單的講, 它只能保證, 一邊執行緒主讀(遍歷/獲取), 一邊執行緒主寫(新增/刪除/修改)操作上的安全。如果一邊執行緒讀寫複合操作,另一邊執行緒也讀寫複合操作,那它也無能為力啦。
案例1:CopyOnWriteArrayList 無法保證非讀寫模式執行緒安全
比如: 上篇測試例子
public class App { public static void main(String[] args) { CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>(); list.add("1"); list.add("2"); new Thread(new Runnable() { public void run() { //集合大小 int len = list.size(); try { //睡5s Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } //刪除最後一個 list.remove(len-1); } }, "t1").start(); new Thread(new Runnable() { public void run() { //清空集合 list.clear(); } }, "t2").start(); } }
將ArrayList換成CopyOnWriteArrayList, 依然存在陣列越界問題, 原因就是上面說的CopyOnWriteArrayList僅保證一執行緒主讀, 一執行緒主寫, , 例子中t1執行緒複合操作,基友讀又有寫難於保證操作安全.
案例2:ConcurrentModificationException 異常解析
再看下面例子: 一個執行緒forEach遍歷list集合, 另一執行緒刪除list集合元素
public class App { public static void main(String[] args) { //CopyOnWriteArrayList final Vector<String> list = new Vector<>(); //final ArrayList<String> list = new ArrayList<>(); for(int i =0;i < 1000; i++){ list.add(i+""); } new Thread(new Runnable() { public void run() { for(String s : list){ System.out.println(s); } } }, "t1").start(); new Thread(new Runnable() { public void run() { for (int i = 0; i < list.size(); i++) { list.remove(i); } } }, "t2").start(); } }
使用普通的ArrayList 或者使用單操作執行緒安全的Vector, 多次執行,有很大概率丟擲一個非常經典的執行緒異常:
Exception in thread "t1" java.util.ConcurrentModificationException
at java.util.Vector$Itr.checkForComodification(Vector.java:1184)
at java.util.Vector$Itr.next(Vector.java:1137)
at cn.wolfcode.ch14.App$1.run(App.java:19)
at java.lang.Thread.run(Thread.java:745)
分析:
ConcurrentModificationException 異常是Collection 集合定義一個用於限制多執行緒環境下集合併發修改的異常.比如: 某個執行緒在 Collection 上進行迭代時,另一個執行緒試圖修改該 Collection。在這些情況下,第一執行緒迭代結果就不確定了。所以當檢測到這種行為,迭代器選擇丟擲此異常。
來看下原始碼, 它是怎麼折騰: 以ArrayList為例
ArrayList:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
//關鍵屬性:繼承自父類AbstractList
protected transient int modCount = 0;
}
modCount 屬性繼承自AbstractList, 用於記錄的是集合被修改(任意能導致集合結構發生變化的操作)次數, 每次修改+1.
public boolean add(E e) {
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
list集合的forEach迭代, 底層使用迭代器模式實現的, 在Arraylist內部維護了一個迭代器類
private class Itr implements Iterator<E> {
int cursor; //迭代器遊標, 指向下一個元素索引
int lastRet = -1; // 最後一個元素索引
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
在list呼叫forEach操作時回撥用iterator 方法獲取迭代器物件,
public Iterator<E> iterator() {
return new Itr();
}
此時回將list持有的modCount賦值給itr類中的expectedModCount 屬性,當使用迭代器迭代list元素時, iter.next( ) 方法會執行, 執行前呼叫checkForComodification 方法, 檢查modCount 是否等於expectedModCount
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
如果A執行緒在執行iter.next() 方法前, B執行緒對list集合做修改動作, 此時的modCount 就無法保證等於 expectedModCount , 故丟擲ConcurrentModificationException異常.
回到案例, 執行緒t1做迭代操作, 執行緒t2做刪除操作, 執行緒t2會對modCount進行修改, t1迭代器初始化的expectedModCount 屬性, 肯定無法等於modCount, 所以丟擲ConcurrentModificationException異常.
案例中, list集合使用丟擲ConcurrentModificationException阻止存在併發安全隱患的迭代. 那如果, 存在這麼一場景, 執行緒t1只關心當前時刻持有的list集合的資料迭代. 不關心list的資料是否被修改呢?此時,可以使用CopyOnWriteArrayList作為操作的集合
public class App {
public static void main(String[] args) {
//final Vector<String> list = new Vector<>();
final CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
for(int i =0;i < 1000; i++){
list.add(i+"");
}
new Thread(new Runnable() {
public void run() {
for(String s : list){
System.out.println(s);
}
}
}, "t1").start();
new Thread(new Runnable() {
public void run() {
for (int i = 0; i < list.size(); i++)
{
list.remove(i);
}
}
}, "t2").start();
}
}
上述案例如果使用CopyOnWriteArrayList作為迭代集合, 執行緒t1可以很愉快輸出0~1000的資料, 它是怎麼做到? 這得從CopyOnWriteArrayList原理說起.
CopyOnWriteArrayList類的設計理念,是基於一種叫Copy-On-Write(簡稱COW)設計思路,通俗的說就是讀與寫分離。具體可以看下圖:
最初CopyOnWriteArrayList使用array資料存放list集合資料
當使用迭代器對CopyOnWriteArrayList進行遍歷, 迭代器會給array弄一個快照,snapshot指向與array陣列資料.具體程式碼:CopyOnWriteArrayList 原始碼
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}
static final class COWIterator<E> implements ListIterator<E> {
/** Snapshot of the array */
private final Object[] snapshot;
private int cursor;
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}
}
此時, 如果有其它執行緒對相同的list做寫操作, 比如add元素
CopyOnWriteArrayList 將array複製一份賦值個newElments,然後在新的陣列中做寫操作, 操作結束後, 讓array重新指向新陣列.
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();
}
}
final Object[] getArray() {
return array;
}
final void setArray(Object[] a) {
array = a;
}
注意觀察, 在寫操作時, 使用lock.lock();進行加鎖, 保證寫安全.
回到上面案例, 執行緒t1在迭代時一直使用的最初的array快照snapshot, 執行緒t2的寫操作修改的都是新的array對snapshot沒有任何影響, 所以可以從0列印到999
最後說下CopyOnWriteArrayList 的侷限性:
1>每次寫操作都需要複製, 記憶體佔用比重高, 不適宜操作大量資料
2>無法保證資料實時一致, 只能保證最終資料一致.
所以CopyOnWriteArrayList 在讀遠大於寫的場合使用才有一定意義.
到這本篇結束.