1. 程式人生 > >JUC包-原子類(AtomicInteger為例)

JUC包-原子類(AtomicInteger為例)

[TOC] # JUC包-原子類 ## **為什麼需要JUC包中的原子類** 首先,一個簡單的i++可以分為三步: 1. 讀取i的值 2. 計算i+1 3. 將計算出i+1賦給i 這就無法保證i++的原子性,即在i++過程中,可能會出現其他執行緒也讀取了i的 值,但讀取到的不是更改過後的i的值。 ## **原子類原理(AtomicInteger為例)** 原子類的原子性是通過**volatile** + **CAS**實現原子操作的。 ### **volatile** ![AtomicInteger原始碼](https://img2020.cnblogs.com/blog/2243359/202101/2243359-20210105204315630-389631320.png) AtomicInteger類中的value是有**volatile關鍵字**修飾的,這就保證了value的記憶體可見性,這為後續的CAS實現提供了基礎。 ### **CAS** 通過檢視原始碼可以發現,AtomicInteger類的**值更新操作**都是通過呼叫 **getAndAddInt(Object var1, long var2, int var4)**方法實現 ```java /** * Atomically adds the given value to the current value. * * @param delta the value to add * @return the previous value */ public final int getAndAdd(int delta) { //返回的是修改前的值,類似於i++ return unsafe.getAndAddInt(this, valueOffset, delta); } /** * Atomically adds the given value to the current value. * * @param delta the value to add * @return the updated value */ public final int addAndGet(int delta) { //返回的是更新後的值,類似於++i return unsafe.getAndAddInt(this, valueOffset, delta) + delta; } ``` 當我們檢視getAndAddInt方法的具體實現,可以發現在整個方法中存在一個循 環,這就是我們說的自旋鎖,顧名思義,while語句裡面的條件一直為true,這個 迴圈就會一直執行下去。 ```java 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; } ``` 下面我們來分析getAndAddInt方法中的各個引數的具體含義: > Object var1:this,表示當前物件 > long var2:valueOffset,表示當前物件的記憶體偏移量 > int var4:delat,需要加上的數值 所以整個方法的執行流程可以歸納為: 1. 讀取傳入物件this在主存中偏移量為offset位置的值賦值給var5 2. 將var5的值與當前執行緒物件記憶體中偏移量為offset位置的值進行比較(compare) 3. 如果相等,將var5+var4的值更新到物件記憶體中偏移量為offset位置(swap);如果不 相等,就進入while迴圈自旋。 ### **CAS的缺點** 1. 迴圈時間長,開銷大 > 在併發量比較高的情況下,如果許多執行緒反覆嘗試更新某一個變數,卻 > > 又一直更新不成功,迴圈往復,會給CPU帶來很大的壓力 2. 只能保證一個共享變數的原子性操作 > CAS機制所保證的只是一個變數的原子性操作,而不能保證整個程式碼塊 > > 的原子性。比如需要保證3個變數共同進行原子性的更新,就不得不使用 > > Synchronized了 3. ABA問題 > 見下文 ## **ABA問題** ### **什麼是ABA問題** 簡單來說就是CAS過程只在乎**當前值**與**期望值**是否相等,只在乎最終結果,不考慮中 間變化,具體可以看下面一個簡單的例子。 ```java public class Test { static AtomicInteger atomicInteger = new AtomicInteger(0); public static void main(String[] args) { atomicInteger.compareAndSet(0,1); System.out.println("執行緒A第一次修改:0->" + atomicInteger.get()); new Thread(() -> { atomicInteger.compareAndSet(1,0); System.out.println("執行緒A第二次修改:1->" + atomicInteger.get()); }, "testA").start(); new Thread(() -> { try { //確保A執行緒修改完畢 TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } atomicInteger.compareAndSet(0,2); System.out.println("執行緒B第一次修改:0->" + atomicInteger.get()); }, "testB").start(); } } ``` 程式執行後輸出的結果,由此可見AtomicInteger的CAS中間步驟有變化,但是沒有被感知到。 ![ABA問題](https://img2020.cnblogs.com/blog/2243359/202101/2243359-20210106193703524-262873699.png) ### **ABA問題的解決辦法** 一個簡單的想法是,在資料上加上時間戳(版本號),使得執行緒每次對變數進行修改時,不僅要對比值,還要 對比時間戳(版本號),每次修改操作都會導致時間戳(版本號)改變為新的 值; 我們通過**AtomicStampedReference**類引入版本號,如下圖所示 ```java public class Test { //初始化數值為0,版本號為1 static AtomicStampedReference atomicStampedReference = new AtomicStampedReference<>(0, 1); public static void main(String[] args) { new Thread(() -> { /* compareAndSet四個引數分別為 * 期望值/新的值/期望版本號/新的版本號 */ atomicStampedReference.compareAndSet(0, 1, 1, 2); System.out.println("數值第一次修改為" + atomicStampedReference.getReference() + " 版本號第一次修改為" + atomicStampedReference.getStamp()); atomicStampedReference.compareAndSet(1, 0, 2, 3); System.out.println("數值第二次修改為" + atomicStampedReference.getReference() + " 版本號第二次修改為" + atomicStampedReference.getStamp()); }, "testA").start(); new Thread(() -> { try { //確保A執行緒修改完畢 TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } atomicStampedReference.compareAndSet(0, 1, 1, 2); System.out.println("數值第三次修改為" + atomicStampedReference.getReference() + " 版本號第三次修改為" + atomicStampedReference.getStamp()); }, "testB").start(); } } ``` 上述程式碼的程式執行結果如下圖所示,可以看到當第三次修改的時候,雖然期望值0匹配,但是期望版本號不匹配,導致第三次修改無效。 ![](https://img2020.cnblogs.com/blog/2243359/202101/2243359-20210106195712575-669168