這是我參考的一篇文章《基於CAS的樂觀鎖實現》,講述的是一種需要CPU支持的執行技術CAS(Compare and Swap)。 首先理解什麼是原子性操作,意思是不能再拆分的操作,例如改寫一個值,讀取一個值都屬於原子性操作。 那麼CAS是兩個操作,先比較舊值,比較通過後再進行改寫,這種連合操作合併成 ...
這是我參考的一篇文章《基於CAS的樂觀鎖實現》,講述的是一種需要CPU支持的執行技術CAS(Compare and Swap)。
首先理解什麼是原子性操作,意思是不能再拆分的操作,例如改寫一個值,讀取一個值都屬於原子性操作。
那麼CAS是兩個操作,先比較舊值,比較通過後再進行改寫,這種連合操作合併成一個指令交給CPU,由CPU操作來確保這是一個原子性操作。
多線程同時改寫同一個值時,每個線程攜帶自己的舊值和新值交給CPU改寫,CPU的運行是按逐條指令運行,如果發現舊值不符合,線程就會收到改寫失敗回應。
public final int incrementAndGet() { for (;;) { int current = get(); int next = current + 1; if (compareAndSet(current, next)) return next; } }
因此AtomicInteger#incrementAndGet()方法里,會迴圈嘗試使用compareAndSet(...)方法,直到成功為止。
相關的原子類所在包:java.util.concurrent.atomic
- Atomic + Boolean/Integer/Long/Reference
操作一個對應的類型對象 -
Atomic + Integer/Long/Reference + Array
操作一個對應類型的數組 - Atomic + Integer/Long/Reference + FieldUpdater
操作一個對應類型的Field對象,類似反射方式改寫對象欄位
這裡《AtomicStampedReference解決ABA問題》,講述了原子類會出現ABA時帶來的隱患,文中舉了一個例子,一個單向的鏈表實現了堆棧操作,使用一個原子變數作為鏈頭指針,現在鏈頭是A,A的next是B。有兩個線程分別是T1和T2,他們併發的操作如下:
- T1:AB => B
- T2:AB => B => 空 => D => CD => ACD
由於線程T1只認鏈頭是不是A,如果是A,就會將鏈頭指向B,因此可能會出現直接把ACD變成B,這就是ABA併發的隱患。
- AtomicMarkableReference
操作一個對象類型和boolean類型的二元組 - AtomicStampedReference
操作一個對象類型和int類型的二元組
雖然例子中鏈頭指針是一個原子變數,會出現ABA的情況,但如果再增加一個原子變數,這個原子變數確保不會出現ABA的情況,兩個原子變數作為二元組進行原子性操作,即使用AtomicStampedReference就可以有效解決這個ABA的隱患了。
以下是Java8增加的原子類:
- Striped64
- Long/Double + Adder
- Long/Double + Accumulator
這裡有兩篇文章,內容是分析它們的源碼:《從LongAdder 看更高效的無鎖實現》,《LongAdder和LongAccumulator》
源碼有些複雜,我也沒看完,Striped64是這項新原子類的基類,它提供的原理是,把一個原子數拆分成多個原子數,最後把這多個原子數合成一個數。換句話說,原本在一個數上做遞增或者遞減操作的,現在變成在多個數里,選擇其中一個做做遞增或遞減操作,那麼加起來的結果與原本方式的結果是等價的。雖然是等價,但它結算結果的過程,是需要把多個數加起來,這個過程已經不是線程安全了,所以它的應用場合相比原本方式會寬一點,原本方式所取出來的值可以作為唯一ID,但現在方式只能用於統計。試想,如果有1000個線程同時在統計同一個數據,那麼原本方式的原子類,就會失敗率上升,效率也會隨之下降。但如果把1000個線程,分成10份,每100個線程統計同一個數據,那麼產生10個數據,最後統計的結果就是這10個數據疊加一起的結果,失敗率當然因份數的增加而減少,效率也自然有保障。
應用場合:高併發,統計數據。