併發程式設計005 --- 原子類的使用和原理
原子操作是指不會被執行緒排程機制打斷的操作,也就是說在原子操作期間,不會出現執行緒上下文切換;
JDK在java.util.concurrent.atomic包中提供了多個原子類,如下:
其中從DoubleAccumulator開始,是JDK1.8提供的採用分段思想的高效能原子類;
在多執行緒場景中,不可避免的會有資料的加減運算,很顯然這些操作不是執行緒安全的;我們可以通過synchronized、Lock等方式保證執行緒安全,但是這些加鎖操作大多數情況下會降低執行效率;另外的一種方法就是採用CAS + 自旋鎖來解決執行緒安全問題;
CAS --- Compare And Swap,先去記憶體中獲取當前變數值,如果是預期的值,更新為新的值;
自旋鎖 ---- 當執行緒嘗試獲取鎖失敗時,該執行緒可以阻塞,等待所釋放後OS的排程,還可以執行一個空迴圈,不斷的佔用當前CPU,嘗試獲取鎖,後面一種即是自旋鎖;但是其存在一些弊端,如果自旋鎖執行時間過長,會導致CPU資源長時間被佔用,而且該資源消耗會大於執行緒上下文切換,因此,需要根據場景區分使用諮自旋鎖;
上述原子類底層即採用了CAS + 自旋鎖來解決執行緒安全問題
下面以AtomicInteger的常用方法為例說明下原子類的基本用法:
public static void main(String[] args) { final AtomicInteger atomicInteger = newAtomicInteger(1); System.out.println(atomicInteger.incrementAndGet()); }
毫無疑問,執行結果會是2,進入incrementAndGet方法原始碼,瞭解下原子類底層原理
public final int incrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, 1) + 1; }
public final int getAndAddInt(Object var1, long var2, int var4) {int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; }
這裡面有個很重要的變數:valueOffset,指的是變數在記憶體中的偏移量;
getAndAddInt方法的流程為,先從主記憶體中讀取變數值,然後呼叫CAS方法,直到記憶體中的變數值為預期值;
CAS演算法的思想就是,先從記憶體中取出某個時刻的值A,在下一個時刻將該值與記憶體中的值相比較,如果沒有變化,則進行更新;那麼在時間差內,其它的執行緒可能將記憶體中的值先由A修改為B,然後再修改為A;原來的執行緒在比較時,發現取值沒有發生變化,進行了更新;這就是CAS的“ABA”問題;
正常的運算場景下,ABA不會引入業務問題,但是某些場景下,ABA會導致問題,比如:
單向連結串列A->B構成的棧,A是棧頂,現在要棧頂更新為B,前提是棧頂為A
1、執行緒1,先獲取棧頂的值,為A;
2、這時執行緒2將A、B出棧,然後在棧中分別入棧D、C、A,此時棧結構如下: A->C->D
3、執行緒1,執行比較,發現棧頂為A,因此更新棧頂為B,但是此時B的next為空,偏離了預期
為了解決ABA問題,java引入了帶“版本號”的原子類AtomicMarkableReference和AtomicStampedReference,如果出現ABA的場景,那麼變數的版本號加1,並且在比較時認為不是同一個值