Java原子類操作原理剖析

Java學習錄發表於2019-04-01


CAS的概念

對於併發控制來說,使用鎖是一種悲觀的策略。它總是假設每次請求都會產生衝突,如果多個執行緒請求同一個資源,則使用鎖寧可犧牲效能也要保證執行緒安全。而無鎖則是比較樂觀的看待這個問題,它會假設每次訪問都沒有衝突,這樣就提高了效率。但是事實難料、這個衝突是避免不了的,無鎖也考慮到了肯定會遇到衝突,對於衝突的解決無鎖就使用一種比較交換(CAS)的技術來檢測衝突。一旦檢測到衝突就重試當前操作直到成功為止。


CAS演算法

CAS機制中使用了3個基本運算元CAS(V,E,N):V表示要更新的變數,E表示預期值,N表示新值。

CAS更新一個變數的時候,只有當變數的預期值E和要更新的變數V的實際值相同時,才會將V的值修改為N。

一個簡單的例子:
在記憶體地址V當中,儲存一個值為1的變數。

此時執行緒1想把變數的值增加1.對執行緒1來說,預期值E=1,要修改的新值N=2.

線上程1要提交更新之前,另一個執行緒2搶先一步,把V的值率先更新成了2。

此時執行緒1開始提交更新,首先進行預期值E和變數V的實際值比較,發現E不等於V的實際值,提交失敗。

失敗後執行緒1 重新獲取記憶體地址V的當前值,並重新計算想要修改的值。此時對執行緒1來說,E=2,V=2。這個重新嘗試的過程被稱為自旋。

如果這一次依然在提交時發現被執行緒2把V值更新到了3則再次重複步驟5。此時E=3,V=3

步驟5執行執行完畢後再次更新發現沒有其他執行緒改變V的值。執行緒1進行比較,發現A和V的值是相等的。則執行緒1進行交換,把V的值替換為N,也就是2.


Java中CAS的底層實現

我們看一下AtomicInteger當中常用的自增方法incrementAndGet:

123複製程式碼
public final int incrementAndGet() {
       return unsafe.getAndAddInt(this, valueOffset, 1) + 1; 
  }複製程式碼

這裡涉及到兩個重要的物件,一個是unsafe,一個是valueOffset。

unsafe是什麼東西呢?它JVM為我們提供了一個訪問作業系統的後門,unsafe為我們提供了硬體級別的原子操作。而valueOffset物件,是通過unsafe.objectFiledOffset方法得到,所代表的是AtomicInteger物件value成員變數在記憶體中的偏移量。我們可以簡單的把valueOffset理解為value變數的記憶體地址。

而unsafe的getAndAddInt方法顧名思義就是使用作業系統的原子操作來為我們實現當前的的++操作並把舊值返回回來。因為是返回的舊值所以
incrementAndGet方法返回的資料應該是這個舊值加上1


CAS的缺點

CPU開銷過大
在併發量比較高的情況下,如果許多執行緒反覆嘗試更新某一個變數,卻又一直更新不成功,迴圈往復,會給CPU帶來很到的壓力。
    
不能保證程式碼塊的原子性
CAS機制所保證的知識一個變數的原子性操作,而不能保證整個程式碼塊的原子性。比如需要保證3個變數共同進行原子性的更新,就不得不使用synchronized了。

ABA問題
這是CAS機制最大的問題所在。
複製程式碼

我們現在來說什麼是ABA問題。

假設小王賬戶有1000塊錢,即v=1000。

這時有三個執行緒想使用CAS的方式更新這個小王的賬戶。執行緒1和執行緒2已經獲取當前賬戶餘額為1000,執行緒3還未獲取當前值。

執行緒1為花唄扣款、執行緒2為花唄扣款的備用操作(避免第一次扣款失敗),執行緒3為工資入賬

接下來,執行緒1先一步執行成功,把當前賬戶成功從1000減少到500;同時執行緒2因為某種原因被阻塞住,沒有及時扣款;執行緒3線上程1扣款之後,獲取了當前值500。

在之後,執行緒2仍然處於阻塞狀態,執行緒3繼續執行,成功入賬工資500,把當前值又變回了1000。

此時,執行緒2恢復執行狀態,進行更新之前查詢E和V相同,所以毫不猶豫的進行又一次賬戶扣款。

這種扣款的方式對於小王來說肯定是不可接受的(估計都要瘋了),解決方案就是在操作的時候加個版本號或者是時間戳來標示狀態資訊。

同樣以剛才的例子來說:

假設小王賬戶有1000塊錢,即v=1000。

這時有三個執行緒想使用CAS的方式更新這個小王的賬戶。執行緒1和執行緒2已經獲取當前賬戶餘額為1000,執行緒3還未獲取當前值。但是呢,這裡執行緒1和2還需要記錄一個獲取當前賬戶餘額的最後更新時間,比如9.30.

同樣的執行緒1為花唄扣款、執行緒2為花唄扣款的備用操作(避免第一次扣款失敗),執行緒3為工資入賬。

接下來,執行緒1先一步執行成功,把當前賬戶成功從1000減少到500;此時賬戶餘額的時間戳就已經變了,比如9.31。同時執行緒2因為某種原因被阻塞住,沒有及時扣款;執行緒3線上程1扣款之後,獲取了當前值500和時間戳9.31。

在之後,執行緒2仍然處於阻塞狀態,執行緒3繼續執行,成功入賬工資500,把賬戶又變回了1000,同時時間戳更新為9.32。

此時,執行緒2恢復執行狀態,進行更新之前查詢E和V雖然相同,但是時間戳確是不一樣的。


Java提供的12種原子操作類

原子更新基本型別

AtomicBoolean:原子更新布林型別
AtomicInteger:原子更新整型
AtomicLong:原子更新長整型。複製程式碼



原子更新陣列

複製程式碼
AtomicIntegerArray:原子更新整型陣列裡的元素。
AtomicLongArray:原子更新長整型陣列裡面的元素。
AtomicReferenceArray:原子更新引用型別陣列裡的元素。複製程式碼

原子更新引用型別

AtomicReference:原子更新引用型別。
AtomicReferenceFieldUpdater:原子更新引用型別裡的欄位。
AtomicMarkableReference:原子更新帶有標記位的引用型別。複製程式碼

原子更新欄位

AtomicIntegerFieldUpdater:原子更新整型欄位的更新器。
AtomicLongFieldUpdater:原子更新長整型欄位的更新器。
AtomicStampedReference:原子更新帶有版本號的引用型別。複製程式碼


相關文章