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