CAS原理 進階篇(漫畫版)

原文鏈接:https://blog.csdn.net/bjweimengshu/article/details/79000506

注:本文轉自 程序員小灰 微信公衆號~

基礎篇地址:https://blog.csdn.net/weixin_39788856/article/details/98721367


CAS進階篇

這一期我們來深入介紹之前遺留的兩個問題:

1.Java當中CAS的底層實現

2.CAS的ABA問題和解決方法

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

首先看一看AtomicInteger當中常用的自增方法 incrementAndGet

public final int incrementAndGet() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return next;
    }
}
private volatile int value;
public final int get() {
    return value;
}

這段代碼是一個無限循環,也就是CAS的自旋。循環體當中做了三件事:
1.獲取當前值。
2.當前值+1,計算出目標值。
3.進行CAS操作,如果成功則跳出循環,如果失敗則重複上述步驟。

這裏需要注意的重點是 get 方法,這個方法的作用是獲取變量的當前值。

如何保證獲得的當前值是內存中的最新值呢?很簡單,用volatile關鍵字來保證。有關volatile關鍵字的知識,我們之前有介紹過,這裏就不詳細闡述了。

在這裏插入圖片描述

在這裏插入圖片描述

接下來看一看compareAndSet方法的實現,以及方法所依賴對象的來歷:

在這裏插入圖片描述

compareAndSet方法的實現很簡單,只有一行代碼。這裏涉及到兩個重要的對象,一個是unsafe,一個是valueOffset

什麼是unsafe呢?Java語言不像C,C++那樣可以直接訪問底層操作系統,但是JVM爲我們提供了一個後門,這個後門就是unsafe。unsafe爲我們提供了硬件級別的原子操作

至於valueOffset對象,是通過unsafe.objectFieldOffset方法得到,所代表的是AtomicInteger對象value成員變量在內存中的偏移量。我們可以簡單地把valueOffset理解爲value變量的內存地址。

我們在上一期說過,CAS機制當中使用了3個基本操作數:內存地址V,舊的預期值A,要修改的新值B。

而unsafe的compareAndSwapInt方法參數包括了這三個基本元素:valueOffset參數代表了V,expect參數代表了A,update參數代表了B。

正是unsafe的compareAndSwapInt方法保證了Compare和Swap操作之間的原子性操作。
在這裏插入圖片描述
在這裏插入圖片描述

什麼是ABA呢?假設內存中有一個值爲A的變量,存儲在地址V當中。
在這裏插入圖片描述
此時有三個線程想使用CAS的方式更新這個變量值,每個線程的執行時間有略微的偏差。線程1和線程2已經獲得當前值,線程3還未獲得當前值。
在這裏插入圖片描述
接下來,線程1先一步執行成功,把當前值成功從A更新爲B;同時線程2因爲某種原因被阻塞住,沒有做更新操作;線程3在線程1更新之後,獲得了當前值B。
在這裏插入圖片描述
再之後,線程2仍然處於阻塞狀態,線程3繼續執行,成功把當前值從B更新成了A。
在這裏插入圖片描述
最後,線程2終於恢復了運行狀態,由於阻塞之前已經獲得了“當前值”A,並且經過compare檢測,內存地址V中的實際值也是A,所以成功把變量值A更新成了B。
在這裏插入圖片描述
這個過程中,線程2獲取到的變量值A是一箇舊值,儘管和當前的實際值相同,但內存地址V中的變量已經經歷了A->B->A的改變。
在這裏插入圖片描述
在這裏插入圖片描述

當我們舉一個提款機的例子。假設有一個遵循CAS原理的提款機,小灰有100元存款,要用這個提款機來提款50元。
在這裏插入圖片描述
由於提款機硬件出了點小問題,小灰的提款操作被同時提交兩次,開啓了兩個線程,兩個線程都是獲取當前值100元,要更新成50元。

理想情況下,應該一個線程更新成功,另一個線程更新失敗,小灰的存款只被扣一次。
在這裏插入圖片描述
線程1首先執行成功,把餘額從100改成50。線程2因爲某種原因阻塞了。這時候,小灰的媽媽剛好給小灰匯款50元。
在這裏插入圖片描述
線程2仍然是阻塞狀態,線程3執行成功,把餘額從50改成100。
在這裏插入圖片描述
線程2恢復運行,由於阻塞之前已經獲得了“當前值”100,並且經過compare檢測,此時存款實際值也是100,所以成功把變量值100更新成了50。
在這裏插入圖片描述
在這裏插入圖片描述
這個舉例改編自《java特種兵》當中的一段例子。原本線程2應當提交失敗,小灰的正確餘額應該保持爲100元,結果由於ABA問題提交成功了。
在這裏插入圖片描述

在這裏插入圖片描述

什麼意思呢?真正要做到嚴謹的CAS機制,我們在Compare階段不僅要比較期望值A和地址V中的實際值,還要比較變量的版本號是否一致。

我們仍然以最初的例子來說明一下,假設地址V中存儲着變量值A,當前版本號是01。線程1獲得了當前值A和版本號01,想要更新爲B,但是被阻塞了。

在這裏插入圖片描述
這時候,內存地址V中的變量發生了多次改變,版本號提升爲03,但是變量值仍然是A。
在這裏插入圖片描述
隨後線程1恢復運行,進行Compare操作。經過比較,線程1所獲得的值和地址V的實際值都是A,但是版本號不相等,所以這一次更新失敗。
在這裏插入圖片描述
在Java當中,AtomicStampedReference類就實現了用版本號做比較的CAS機制。
在這裏插入圖片描述
在這裏插入圖片描述

1. Java語言CAS底層如何實現?

利用unsafe提供了原子性操作方法。

2. 什麼是ABA問題?怎麼解決?

當一個值從A更新成B,又更新會A,普通CAS機制會誤判通過檢測。

利用版本號比較可以有效解決ABA問題。


---- END ----

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章