轉載聲明:無法找到原作者,查到後會添加聲明
我們先看一段代碼:
啓動兩個線程,每個線程中讓靜態變量count循環累加100次。
最終輸出的count結果一定是200嗎?因爲這段代碼是非線程安全的,所以最終的自增結果很可能會小於200。我們再加上synchronized同步鎖,再來看一下。
加了同步鎖之後,count自增的操作變成了原子性操作,所以最終輸出一定是count=200,代碼實現了線程安全。雖然synchronized確保了線程安全,但是在某些情況下,這並不是一個最有的選擇。
關鍵在於性能問題。
synchronized關鍵字會讓沒有得到鎖資源的線程進入BLOCKED狀態,而後在爭奪到鎖資源後恢復爲RUNNABLE狀態,這個過程中涉及到操作系統用戶模式和內核模式的轉換,代價比較高。
儘管JAVA 1.6爲synchronized做了優化,增加了從偏向鎖到輕量級鎖再到重量級鎖的過過度,但是在最終轉變爲重量級鎖之後,性能仍然比較低。所以面對這種情況,我們就可以使用java中的“原子操作類”。
所謂原子操作類,指的是java.util.concurrent.atomic包下,一系列以Atomic開頭的包裝類。如AtomicBoolean,AtomicUInteger,AtomicLong。它們分別用於Boolean,Integer,Long類型的原子性操作。
現在我們嘗試使用AtomicInteger類:
使用AtomicInteger之後,最終的輸出結果同樣可以保證是200。並且在某些情況下,代碼的性能會比synchronized更好。
而Atomic操作類的底層正是用到了“CAS機制”。
CAS是英文單詞Compare and Swap的縮寫,翻譯過來就是比較並替換。
CAS機制中使用了3個基本操作數:內存地址V,舊的預期值A,要修改的新值B。
更新一個變量的時候,只有當變量的預期值A和內存地址V當中的實際值相同時,纔會將內存地址V對應的值修改爲B。
我們看一個例子:
1. 在內存地址V當中,存儲着值爲10的變量。
2. 此時線程1想把變量的值增加1.對線程1來說,舊的預期值A=10,要修改的新值B=11.
3. 在線程1要提交更新之前,另一個線程2搶先一步,把內存地址V中的變量值率先更新成了11。
4. 線程1開始提交更新,首先進行A和地址V的實際值比較,發現A不等於V的實際值,提交失敗。
5. 線程1 重新獲取內存地址V的當前值,並重新計算想要修改的值。此時對線程1來說,A=11,B=12。這個重新嘗試的過程被稱爲自旋。
6. 這一次比較幸運,沒有其他線程改變地址V的值。線程1進行比較,發現A和地址V的實際值是相等的。
7. 線程1進行交換,把地址V的值替換爲B,也就是12.
從思想上來說,synchronized屬於悲觀鎖,悲觀的認爲程序中的併發情況嚴重,所以嚴防死守,CAS屬於樂觀鎖,樂觀地認爲程序中的併發情況不那麼嚴重,所以讓線程不斷去重試更新。
在java中除了上面提到的Atomic系列類,以及Lock系列類奪得底層實現,甚至在JAVA1.6以上版本,synchronized轉變爲重量級鎖之前,也會採用CAS機制。
CAS的缺點:
1) CPU開銷過大
在併發量比較高的情況下,如果許多線程反覆嘗試更新某一個變量,卻又一直更新不成功,循環往復,會給CPU帶來很到的壓力。
2) 不能保證代碼塊的原子性
CAS機制所保證的知識一個變量的原子性操作,而不能保證整個代碼塊的原子性。比如需要保證3個變量共同進行原子性的更新,就不得不使用synchronized了。
3) ABA問題
這是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關鍵字來保證(保證線程間的可見性)。我們接下來看一下compareAndSet方法的實現:
compareAndSet方法的實現很簡單,只有一行代碼。這裏涉及到兩個重要的對象,一個是unsafe,一個是valueOffset。
什麼是unsafe呢?Java語言不像C,C++那樣可以直接訪問底層操作系統,但是JVM爲我們提供了一個後門,這個後門就是unsafe。unsafe爲我們提供了硬件級別的原子操作。
至於valueOffset對象,是通過unsafe.objectFiledOffset方法得到,所代表的是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。
看起來這個例子沒啥問題,但如果結合實際,就可以發現它的問題所在。
我們假設一個提款機的例子。假設有一個遵循CAS原理的提款機,小灰有100元存款,要用這個提款機來提款50元。
由於提款機硬件出了點問題,小灰的提款操作被同時提交了兩次,開啓了兩個線程,兩個線程都是獲取當前值100元,要更新成50元。
理想情況下,應該一個線程更新成功,一個線程更新失敗,小灰的存款值被扣一次。
線程1首先執行成功,把餘額從100改成50.線程2因爲某種原因阻塞。這時,小灰的媽媽剛好給小灰匯款50元。
線程2仍然是阻塞狀態,線程3執行成功,把餘額從50改成了100。
線程2恢復運行,由於阻塞之前獲得了“當前值”100,並且經過compare檢測,此時存款實際值也是100,所以會成功把變量值100更新成50。
原本線程2應當提交失敗,小灰的正確餘額應該保持100元,結果由於ABA問題提交成功了。
怎麼解決呢?加個版本號就可以了。
真正要做到嚴謹的CAS機制,我們在compare階段不僅要比較期望值A和地址V中的實際值,還要比較變量的版本號是否一致。
我們仍然以剛纔的例子來說明,假設地址V中存儲着變量值A,當前版本號是01。線程1獲取了當前值A和版本號01,想要更新爲B,但是被阻塞了。
這時候,內存地址V中變量發生了多次改變,版本號提升爲03,但是變量值仍然是A。
隨後線程1恢復運行,進行compare操作。經過比較,線程1所獲得的值和地址的實際值都是A,但是版本號不相等,所以這一次更新失敗。
在Java中,AtomicStampedReference類就實現了用版本號作比較額CAS機制。
1. java語言CAS底層如何實現?
利用unsafe提供的原子性操作方法。
2.什麼事ABA問題?怎麼解決?
當一個值從A變成B,又更新回A,普通CAS機制會誤判通過檢測。
利用版本號比較可以有效解決ABA問題。