Java多線程之CAS機(jī)制詳解
1. 什么是CAS?
CAS 全名 compare and swap (比較并交換)是一種基于 Java 實(shí)現(xiàn)的 計(jì)算機(jī)代數(shù)系統(tǒng),用于多線程并發(fā)編程時(shí)數(shù)據(jù)在無(wú)鎖的情況下保證線程安全安全運(yùn)行。
CAS機(jī)制 主要用于對(duì)一個(gè)變量(操作)進(jìn)行原子性的操作,它包含三個(gè)參數(shù)值:需要進(jìn)行操作的變量A、變量的舊值B、即將要更改的新值C。
CAS機(jī)制 會(huì)對(duì)當(dāng)前內(nèi)存中的 A 進(jìn)行判斷看是否等同于 B ,如果相等則把 A 值更改為 C ,否則不進(jìn)行操作。以下為 CAS 操作的一段偽代碼:
boolean CAS(A,B,C) { if (&A == B) { &A = C; return true; } return false; }
當(dāng)然,以上代碼不具有原子性只是簡(jiǎn)單理解 CAS 的判定以及返回機(jī)制。真正的 CAS 只是一條 CPU 指令,相比于上述代碼具有原子性 。
在了解 CAS 的基本判定后下面我們來(lái)看如何通過(guò) Java 標(biāo)準(zhǔn)庫(kù)來(lái)運(yùn)用 CAS 。
2. CAS的應(yīng)用
2.1 實(shí)現(xiàn)原子類
CAS 可以不加鎖保證操作的原子性,Java 標(biāo)準(zhǔn)庫(kù)提供了 Atomic + 包裝類,相關(guān)的組合類來(lái)實(shí)現(xiàn)原子操作,這些類都是在 java.util.concurrent.atomic 包底下的。
以常用的 AtomicInteger 類來(lái)舉例,AtomicInteger 類底下的 getAndIncrement 方法達(dá)到的效果就是自增類似于 i++ 操作,getAndDecrement 方法就是自減類似于 i-- 操作。
因此 AtomicInteger 類常見(jiàn)的方法有:
- getAndIncrement 方法,自增操作,類似于 i++。
- getAndDecrement 方法,自減操作,類似于 i--。
- get 方法,獲取當(dāng)前 AtomicInteger 類引用的值。
當(dāng)然,Atomic + 其他“數(shù)值”包裝類也能使用以上方法!
代碼案例,不使用 synchronized 的情況下保證一個(gè)線程自增5000,另一個(gè)線程也自增5000,最后返回兩線程之和10000:
public static void main(String[] args) throws InterruptedException { //初始化number為0 AtomicInteger number = new AtomicInteger(0); //線程1使number自增5000次 Thread thread1 = new Thread(()->{ for (int i = 0; i < 5000; i++) { number.getAndIncrement(); } }); //線程2也使number自增5000次(在線程1執(zhí)行后) Thread thread2 = new Thread(()->{ for (int i = 0; i < 5000; i++) { number.getAndIncrement(); } }); thread1.start();//啟動(dòng)線程1 thread2.start();//啟動(dòng)線程2 thread1.join();//等待線程1執(zhí)行完畢 thread2.join();//等下線程2執(zhí)行完畢 System.out.println(number.get());//輸出number的值 }
運(yùn)行后打?。?/p>
以上代碼,在不使用鎖(synchronized)的情況下保證了線程的安全性。其底層運(yùn)用的就是 CAS 機(jī)制,getAndIncrement 方法的具體實(shí)現(xiàn),我們可以參考以下 偽代碼 來(lái)理解:
class MyAtomicInteger { private int value; public int getAndIncrement() { int oldValue = value; while (CAS(value,oldValue,oldValue + 1) != true) { oldValue = value; } return oldValue; } }
假設(shè) getAndIncrement 方法被兩個(gè)線程同時(shí)調(diào)用,線程1 和 線程2 的 oldValue 值都為 0,內(nèi)存中的 value 值為0。
1)線程1 進(jìn)入了 getAndIncrement 方法,此時(shí)線程1進(jìn)行 CAS 判定,發(fā)現(xiàn)線程1的 oldValue = value,就把 value 進(jìn)行自增。
2) 線程2 進(jìn)入了 getAndIncrement 方法,此時(shí) 線程2 進(jìn)行 CAS 判定,發(fā)現(xiàn) oldValue != value,進(jìn)入 while 循環(huán),把 value 賦值給 old Value。
3)經(jīng)過(guò)以上判斷后,線程2 再次進(jìn)行 CAS 判斷時(shí),發(fā)現(xiàn) oldValue = value 了,此時(shí)的 value 值又會(huì)自增。
以上的 偽代碼 就能實(shí)現(xiàn)一個(gè)原子類,里面的 getAndIncrement 方法也是具備原子性的。通過(guò)上述圖例就能很好的理解。
2.2 實(shí)現(xiàn)自旋鎖
CAS的自旋鎖指的是在使用CAS操作時(shí),當(dāng)CAS操作失敗后,線程不直接阻塞等待,而是繼續(xù)嘗試執(zhí)行CAS操作,即對(duì)前一次CAS操作的失敗進(jìn)行重試,直到CAS操作成功為止。
自旋鎖的意思是程序使用循環(huán)來(lái)等待特定條件的實(shí)現(xiàn)方式,相較于傳統(tǒng)的阻塞鎖,自旋鎖不會(huì)使線程進(jìn)入阻塞狀態(tài),因此避免了線程上下文切換帶來(lái)的開(kāi)銷。通常,當(dāng)線程競(jìng)爭(zhēng)的資源空閑等待的時(shí)間不長(zhǎng),自旋鎖是一種比較高效的同步機(jī)制。
CAS 自旋鎖體現(xiàn):一段 偽代碼 :
public class SpinLock { private Thread owner = null; public void lock(){ // 通過(guò) CAS 看當(dāng)前鎖是否被某個(gè)線程持有. // 如果這個(gè)鎖已經(jīng)被別的線程持有, 那么就自旋等待. // 如果這個(gè)鎖沒(méi)有被別的線程持有, 那么就把 owner 設(shè)為當(dāng)前嘗試加鎖的線程. while(!CAS(this.owner, null, Thread.currentThread())){ } } public void unlock (){ this.owner = null; } }
Thread.currentThread() 為當(dāng)前對(duì)象的引用,以上代碼進(jìn)行 CAS 判定時(shí):
如果判斷 this.owner 為空,則把當(dāng)前對(duì)象的引用賦值給 this.owner。此時(shí) CAS 方法返回 true,并取反,while 循環(huán)退出。判斷 this.owner 不為空,則不做任何操作,CAS 方法返回 false,并取反,while 循環(huán)繼續(xù)執(zhí)行。由于 while 循環(huán)體內(nèi)沒(méi)有任何內(nèi)容,while 條件判斷會(huì)執(zhí)行很快,直到 this.owner 加鎖成功為止。
這就是自旋鎖的體現(xiàn),關(guān)于鎖的策略在本專欄中有詳細(xì)講解。大家可以前去查找。
3. CAS的ABA問(wèn)題
ABA 問(wèn)題是:當(dāng)線程1首先讀取到共享變量值A(chǔ)。然后線程2先把這個(gè)共享變量值修改為B,再修改回A。
此時(shí)其他線程再進(jìn)行 CAS 操作時(shí)誤以為共享變量值沒(méi)有被修改過(guò),從而成功的將共享變量更改為新值。
但實(shí)際過(guò)程中共享變量經(jīng)歷了 由 A 變?yōu)?B,再由 B 變?yōu)?A,這樣就可能會(huì)導(dǎo)致一些問(wèn)題。
類似于,網(wǎng)上購(gòu)買(mǎi)一部二手機(jī)。買(mǎi)的時(shí)候,賣(mài)家說(shuō)是零件完好,到手后才發(fā)現(xiàn)是一部翻新機(jī)。這樣就會(huì)導(dǎo)致手機(jī)用不了幾天就出問(wèn)題。至于到手之前,賣(mài)家不說(shuō)是識(shí)別不出這部手機(jī)的好壞的。
3.1 ABA問(wèn)題可能引起的BUG
ABA 問(wèn)題,就是 CAS 機(jī)制導(dǎo)致的數(shù)據(jù)反復(fù)橫跳。
假設(shè),張三要去 ATM 取錢(qián),張三余額有 1000 元,他要取 500 元。他安排兩個(gè)線程,線程1 和 線程2 來(lái)并發(fā)執(zhí)行取錢(qián)操作。
預(yù)期效果:線程1 執(zhí)行取錢(qián)操作判斷余額為 1000,執(zhí)行余額 -500 操作,此時(shí)余額 500,線程2 處于阻塞等待狀態(tài)。當(dāng) 線程2 執(zhí)行取錢(qián)操作判斷余額不是 1000 不執(zhí)行 -500 操作。
ABA問(wèn)題出現(xiàn):線程 1 執(zhí)行取錢(qián)操作判斷余額為 1000,執(zhí)行余額 -500 操作,此時(shí)余額 500,線程2 阻塞等待狀態(tài)。突然,張三的朋友給他轉(zhuǎn)賬了 500 ,此時(shí) 余額又變回了 1000。
線程2 進(jìn)入取錢(qián)操作時(shí),判斷余額為 1000 元,執(zhí)行余額 -500 操作,此時(shí)余額剩余 500。這就是 ABA 問(wèn)題造成的后果,張三回家后打開(kāi)手機(jī)查看余額剩余 500,實(shí)際張三被 ABA 問(wèn)題坑了 500元。
3.2 解決ABA問(wèn)題
CAS 操作,是將需要改變的值 A 與舊值 B 進(jìn)行比較,相等則把新值 C 賦值給 A ,否則不做改變。解決 CAS 出現(xiàn) ABA 問(wèn)題,我們可以引入一個(gè)版本號(hào),比較版本號(hào)是否符合預(yù)期。
比如在網(wǎng)上購(gòu)買(mǎi)一部二手機(jī),賣(mài)家會(huì)將手機(jī)的翻新程度進(jìn)行一個(gè)版本號(hào)標(biāo)記,翻新1次記版本號(hào)1,翻新2次的記版本號(hào)2,以此類推。這時(shí)候,客戶會(huì)根據(jù)版本號(hào)來(lái)選擇翻新程度相應(yīng)的手機(jī)。
- 當(dāng)版本號(hào)和讀到的版本號(hào)相等,則修改數(shù)據(jù),并把版本號(hào) + 1。
- 當(dāng)版本號(hào)高于讀到的版本號(hào),就操作失?。ㄕJ(rèn)為數(shù)據(jù)已經(jīng)被修改過(guò)了)
根據(jù)以下 偽代碼 來(lái)理解:
num = 0; version = 1; old = version; CAS(version,old,old+1,num); public void CAS(version,oldVersion,oldVersion+1,num){ if(version == oldVersion) { version = oldVersion + 1; num++; } }
對(duì)以上代碼進(jìn)行一個(gè)講解, version 作為版本號(hào),當(dāng) version 版本號(hào)等于讀到的 oldVersion 版本號(hào),則把 oldVersion +1 賦值給 version,并且 num ++ 。這樣就能避免 ABA 問(wèn)題的出現(xiàn)。
當(dāng)然,Java 中 提供了一個(gè) AtomicStampedReference<>類,這個(gè)類可以對(duì)某個(gè)類進(jìn)行保證,這樣就能提供上述的版本號(hào)管理功能。
public class TestDemo { private static final AtomicStampedReference<Integer> sharedValue = new AtomicStampedReference<>(10, 0); public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread(() -> { int expectedStamp = sharedValue.getStamp(); int newValue = 20; sharedValue.compareAndSet(10, newValue, expectedStamp, expectedStamp + 1); System.out.println(Thread.currentThread().getName() + " updated sharedValue to " + newValue); }, "Thread-1"); Thread thread2 = new Thread(() -> { int expectedStamp = sharedValue.getStamp(); int oldValue = sharedValue.getReference(); int newValue = 30; sharedValue.compareAndSet(oldValue, newValue, expectedStamp, expectedStamp + 1); System.out.println(Thread.currentThread().getName() + " updated sharedValue to " + newValue); }, "Thread-2"); thread1.start(); thread1.join(); thread2.start(); thread2.join(); System.out.println("final value: " + sharedValue.getReference()); } }
運(yùn)行后打印:
以上代碼,共享變量的初始值為10,然后線程1將共享變量的值修改為20,線程2將共享變量的值修改為30。由于AtomicStampedReference類包含版本號(hào)信息,因此即使共享變量的值在這個(gè)過(guò)程中發(fā)生了ABA的變化,CAS操作也可以正常進(jìn)行,不會(huì)出現(xiàn)誤判現(xiàn)象。
談?wù)勀銓?duì) CAS 機(jī)制的理解?
CAS 全稱 compare and swap 即比較并交換,它通過(guò)一個(gè)原子的操作完成“讀取內(nèi)存,比較是否相等,修改內(nèi)存”這三個(gè)步驟,本質(zhì)上需要 CPU 指令的支持。
ABA 問(wèn)題如何解決?
我們可以給修改的數(shù)據(jù)加上一個(gè)版本號(hào),初始化當(dāng)前版本號(hào)與舊的版本號(hào)相等。判斷當(dāng)前版本號(hào)如果等于舊版本號(hào)則對(duì)數(shù)據(jù)進(jìn)行修改,并使版本號(hào)自增。判斷當(dāng)前版本號(hào)大于舊版本號(hào),則不進(jìn)行任何操作。
到此這篇關(guān)于Java多線程之CAS機(jī)制詳解的文章就介紹到這了,更多相關(guān)CAS機(jī)制詳解內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳細(xì)分析Java中String、StringBuffer、StringBuilder類的性能
在Java中,String類和StringBuffer類以及StringBuilder類都能用于創(chuàng)建字符串對(duì)象,而在分別操作這些對(duì)象時(shí)我們會(huì)發(fā)現(xiàn)JVM執(zhí)行它們的性能并不相同,下面我們就來(lái)詳細(xì)分析Java中String、StringBuffer、StringBuilder類的性能2016-05-05java編程兩種樹(shù)形菜單結(jié)構(gòu)的轉(zhuǎn)換代碼
這篇文章主要介紹了java編程兩種樹(shù)形菜單結(jié)構(gòu)的轉(zhuǎn)換代碼,首先介紹了兩種樹(shù)形菜單結(jié)構(gòu)的代碼,然后展示了轉(zhuǎn)換器實(shí)例代碼,最后分享了相關(guān)實(shí)例及結(jié)果演示,具有一定借鑒價(jià)值,需要的朋友可以了解下。2017-12-12IDEA如何一鍵部署SpringBoot項(xiàng)目到服務(wù)器
文章介紹了如何在IDEA中部署SpringBoot項(xiàng)目到服務(wù)器,使用AlibabaCloudToolkit插件進(jìn)行配置部署,步驟包括設(shè)置服務(wù)名稱、選擇文件上傳類型、選擇jar文件、添加服務(wù)器信息、輸入上傳路徑、選擇上傳后執(zhí)行的腳本以及執(zhí)行前的操作命令2024-12-12Java獲取e.printStackTrace()打印的信息方式
這篇文章主要介紹了Java獲取e.printStackTrace()打印的信息方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-08-08Java與Scala創(chuàng)建List與Map的實(shí)現(xiàn)方式
這篇文章主要介紹了Java與Scala創(chuàng)建List與Map的實(shí)現(xiàn)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-10-10java發(fā)送http請(qǐng)求并獲取狀態(tài)碼的簡(jiǎn)單實(shí)例
下面小編就為大家?guī)?lái)一篇java發(fā)送http請(qǐng)求并獲取狀態(tài)碼的簡(jiǎn)單實(shí)例。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-05-05Struts2實(shí)現(xiàn)自定義攔截器的三種方式詳解
這篇文章主要介紹了Struts2實(shí)現(xiàn)自定義攔截器的三種方式詳解,一些與系統(tǒng)邏輯相關(guān)的通用功能如權(quán)限的控制和用戶登錄控制等,需要通過(guò)自定義攔截器實(shí)現(xiàn),本節(jié)將詳細(xì)講解如何自定義攔截器,需要的朋友可以參考下2023-07-07Java使用poi做加自定義注解實(shí)現(xiàn)對(duì)象與Excel相互轉(zhuǎn)換
這篇文章主要介紹了Java使用poi做加自定義注解實(shí)現(xiàn)對(duì)象與Excel相互轉(zhuǎn)換,對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-05-05SpringBoot-Admin實(shí)現(xiàn)微服務(wù)監(jiān)控+健康檢查+釘釘告警
本文主要介紹了SpringBoot-Admin實(shí)現(xiàn)微服務(wù)監(jiān)控+健康檢查+釘釘告警,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-10-10