对CAS机制的理解(二)
一、Java當(dāng)中CAS的底層實(shí)現(xiàn)
首先看看AtomicInteger的源碼,AtomicInteger中常用的自增方法 incrementAndGet:
這段代碼是一個(gè)無限循環(huán),也就是CAS的自旋。循環(huán)體當(dāng)中做了三件事:
1.獲取當(dāng)前值。
2.當(dāng)前值+1,計(jì)算出目標(biāo)值。
3.進(jìn)行CAS操作,如果成功則跳出循環(huán),如果失敗則重復(fù)上述步驟。
這里需要注意的重點(diǎn)是 get 方法,這個(gè)方法的作用是獲取變量的當(dāng)前值。
如何保證獲得的當(dāng)前值是內(nèi)存中的最新值呢?用volatile關(guān)鍵字來保證。
下面來看compareAndSet方法是如何保證原子性操作的。
compareAndSet方法的實(shí)現(xiàn):
compareAndSet方法的實(shí)現(xiàn)很簡單,只有一行代碼。這里涉及到兩個(gè)重要的對(duì)象,一個(gè)是unsafe,一個(gè)是valueOffset。
什么是unsafe呢?Java語言不像C,C++那樣可以直接訪問底層操作系統(tǒng),但是JVM為我們提供了一個(gè)后門,這個(gè)后門就是unsafe。unsafe為我們提供了硬件級(jí)別的原子操作。
至于valueOffset對(duì)象,是通過unsafe.objectFieldOffset方法得到,所代表的是AtomicInteger對(duì)象value成員變量在內(nèi)存中的偏移量。我們可以簡單地把valueOffset理解為value變量的內(nèi)存地址。
上一篇說過,CAS機(jī)制當(dāng)中使用了3個(gè)基本操作數(shù):內(nèi)存地址V,舊的預(yù)期值A(chǔ),要修改的新值B。
而unsafe的compareAndSwapInt方法參數(shù)包括了這三個(gè)基本元素:valueOffset參數(shù)代表了V,expect參數(shù)代表了A,update參數(shù)代表了B。
正是unsafe的compareAndSwapInt方法保證了Compare和Swap操作之間的原子性操作。
二、CAS的ABA問題和解決方法
所謂ABA問題,就是一個(gè)變量的值從A改成了B,又從B改成了A。
1.假設(shè)內(nèi)存中有一個(gè)值為A的變量,存儲(chǔ)在地址V當(dāng)中。
2.此時(shí)有三個(gè)線程想使用CAS的方式更新這個(gè)變量值,每個(gè)線程的執(zhí)行時(shí)間有略微的偏差。線程1和線程2已經(jīng)獲得當(dāng)前值,線程3還未獲得當(dāng)前值。
3.接下來,線程1先一步執(zhí)行成功,把當(dāng)前值成功從A更新為B;同時(shí)線程2因?yàn)槟撤N原因被阻塞住,沒有做更新操作;線程3在線程1更新之后,獲得了當(dāng)前值B。
4.再之后,線程2仍然處于阻塞狀態(tài),線程3繼續(xù)執(zhí)行,成功把當(dāng)前值從B更新成了A。
5.最后,線程2終于恢復(fù)了運(yùn)行狀態(tài),由于阻塞之前已經(jīng)獲得了“當(dāng)前值”A,并且經(jīng)過compare檢測(cè),內(nèi)存地址V中的實(shí)際值也是A,所以成功把變量值A(chǔ)更新成了B。
這個(gè)過程中,線程2獲取到的變量值A(chǔ)是一個(gè)舊值,盡管和當(dāng)前的實(shí)際值相同,但內(nèi)存地址V中的變量已經(jīng)經(jīng)歷了A->B->A的改變。
這個(gè)例子表面上看似沒什么毛病,因?yàn)楸緛砭褪且袮更新成B。下面結(jié)合實(shí)際應(yīng)用場(chǎng)景,舉一個(gè)提款機(jī)的栗子:
1.假設(shè)有一個(gè)遵循CAS原理的提款機(jī),小灰有100元存款,要用這個(gè)提款機(jī)來提款50元。
2.由于提款機(jī)硬件出了點(diǎn)小問題,小灰的提款操作被同時(shí)提交兩次,開啟了兩個(gè)線程,兩個(gè)線程都是獲取當(dāng)前值100元,要更新成50元。
(理想情況下,應(yīng)該一個(gè)線程更新成功,另一個(gè)線程更新失敗,小灰的存款只被扣一次。)
3.線程1首先執(zhí)行成功,把余額從100改成50。線程2因?yàn)槟撤N原因阻塞了。這時(shí)候,小灰的媽媽剛好給小灰匯款50元。
4.線程2仍然是阻塞狀態(tài),線程3執(zhí)行成功,把余額從50改成100。
5.線程2恢復(fù)運(yùn)行,由于阻塞之前已經(jīng)獲得了“當(dāng)前值”100,并且經(jīng)過compare檢測(cè),此時(shí)存款實(shí)際值也是100,所以成功把變量值100更新成了50。
原本線程2應(yīng)當(dāng)提交失敗,小灰的正確余額應(yīng)該保持為100元,結(jié)果由于ABA問題提交成功了。
ABA問題的解決方法:加個(gè)版本號(hào)。真正要做到嚴(yán)謹(jǐn)?shù)腃AS機(jī)制,我們?cè)贑ompare階段不僅要比較期望值A(chǔ)和地址V中的實(shí)際值,還要比較變量的版本號(hào)是否一致。
仍然以上一篇的栗子來說明,
1.假設(shè)地址V中存儲(chǔ)著變量值A(chǔ),當(dāng)前版本號(hào)是01。線程1獲得了當(dāng)前值A(chǔ)和版本號(hào)01,想要更新為B,但是被阻塞了。
2.這時(shí)候,內(nèi)存地址V中的變量發(fā)生了多次改變,版本號(hào)提升為03,但是變量值仍然是A。
3.隨后線程1恢復(fù)運(yùn)行,進(jìn)行Compare操作。經(jīng)過比較,線程1所獲得的值和地址V的實(shí)際值都是A,但是版本號(hào)不相等,所以這一次更新失敗。
在Java當(dāng)中,AtomicStampedReference類就實(shí)現(xiàn)了用版本號(hào)做比較的CAS機(jī)制。
回顧
1. Java語言CAS底層如何實(shí)現(xiàn)?
利用unsafe提供了原子性操作方法。
2. 什么是ABA問題?怎么解決?
當(dāng)一個(gè)值從A更新成B,又更新會(huì)A,普通CAS機(jī)制會(huì)誤判通過檢測(cè)。
利用版本號(hào)比較可以有效解決ABA問題。
參考:漫畫:什么是CAS機(jī)制?(進(jìn)階篇)
來自公眾號(hào):
轉(zhuǎn)載于:https://www.cnblogs.com/zeroingToOne/p/8955396.html
《新程序員》:云原生和全面數(shù)字化實(shí)踐50位技術(shù)專家共同創(chuàng)作,文字、視頻、音頻交互閱讀總結(jié)
以上是生活随笔為你收集整理的对CAS机制的理解(二)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java 读写Properties配置文
- 下一篇: JAVA中this和super用法