CopyOnWriteArrayList的設計思想非常簡單,但在設計層面有一些小問題需要注意。
JDK版本:oracle java 1.8.0_102
本來不想寫的,但是github上CopyOnWriteArrayList的code results也有165k,為了流量還是寫一寫吧。
實現
看兩個方法你就懂了。
讀元素set()
public E get(int index) {
return get(getArray(), index);
}
final Object[] getArray() {
return array;
}
複製程式碼
get()方法直接呼叫內部的getArray()方法,而getArray()方法則直接返回成員變數array。
我沒明白為什麼要再封裝一層,而不是直接訪問。
array指向一個陣列,是CopyOnWriteArrayList的內部資料結構:
private transient volatile Object[] array;
複製程式碼
敲黑板!!!
**array是一個volatile變數,**其讀、寫操作具有Happends-Before關係。具體來講,執行緒W1通過set()方法“修改”集合後,執行緒R1能立刻通過get()方法得到array的最新值。
你可以理解為volatile變數的讀、寫是原子的,不過,我更希望你能從順序和可見性的角度理解理解volatile、鎖等具有偏序關係的操作。volatile的原理和用法見volatile關鍵字的作用、原理。
寫元素set()
重點是set()方法:
public E set(int index, E element) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
E oldValue = get(elements, index);
if (oldValue != element) {
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len);
newElements[index] = element;
setArray(newElements);
} else {
// Not quite a no-op; ensures volatile write semantics
setArray(elements);
}
return oldValue;
} finally {
lock.unlock();
}
}
final void setArray(Object[] a) {
array = a;
}
複製程式碼
set()方法也很簡單,兩個要點:
- 通過鎖lock保護佇列修改過程
- 在副本上修改,最後替換array引用
按照獨佔鎖的思路,僅僅給寫執行緒加鎖是不行的,會有讀、寫執行緒的競爭問題。但是get()中明明沒有加鎖,為什麼也沒有問題呢?
通過加鎖,保證同一時間最多隻有一個寫執行緒W1進入try block;假設要設定的值與舊值不同。9-10行首先將資料複製一份(此時,沒有其他寫執行緒能進入try block修改集合),11行在副本上修改相應元素,12行修改array引用。array是volatile變數,所以寫的最新值對其他讀執行緒、寫執行緒都是可見的。
這就是所謂的“寫時複製
”。
其他問題
15行volatile寫的作用
實際上,15行的volatile寫是多餘的。這只是為了能從程式碼裡理解到volatile寫的語義,並不必要的保證什麼——不過這種考慮也是不恰當的,反而使程式碼迷惑。一個類似的例子是addIfAbsent():
public boolean addIfAbsent(E e) {
Object[] snapshot = getArray();
return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
addIfAbsent(e, snapshot);
}
private boolean addIfAbsent(E e, Object[] snapshot) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] current = getArray();
int len = current.length;
if (snapshot != current) {
// Optimize for lost race to another addXXX operation
int common = Math.min(snapshot.length, len);
for (int i = 0; i < common; i++)
if (current[i] != snapshot[i] && eq(e, current[i]))
return false;
if (indexOf(e, current, common, len) >= 0)
return false;
}
Object[] newElements = Arrays.copyOf(current, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
複製程式碼
基本思想相同,17、19行都是直接返回,並沒有做多餘的“volatile寫”。
在網上搜的話,還有很多其他觀點。如果你認為我的觀點是錯誤的,歡迎交流。
addIfAbsent的編碼風格跟set()區別很大,不像一個人寫的。需要認識到,JDK是一個發展、變化的產品,一個包、甚至一個類都可能不是同一個人、同一段時間寫的,編碼風格、設計思想可能發生變化;更不要假定JDK的實現一定是對的(當然,絕大部分時候是對的),要基於正確的邏輯去分析,再做判斷。
為什麼必須要給set加鎖?
TODO 20171024
看起來,如果不給set加鎖,似乎併發效能更高,一致性也沒有削弱多少。未解決,歡迎交流。
設計思想
最後總結CopyOnWriteArrayList的設計思想:
- 用併發訪問“陣列副本的引用”代替併發訪問“陣列元素的引用”,大大降低了維護執行緒安全的難度。
- 當前副本可能是失效的,但一定是集合在某一瞬間的快照(一定程度上滿足不變性),滿足弱一致性。
本文連結:原始碼|併發一枝花之CopyOnWriteArrayList
作者:猴子007
出處:monkeysayhi.github.io
本文基於 知識共享署名-相同方式共享 4.0 國際許可協議釋出,歡迎轉載,演繹或用於商業目的,但是必須保留本文的署名及連結。