目錄
- CAS是什么
- CAS的應用
- 實現原子類
- 實現自旋鎖
- ABA問題
- ABA問題概述
- ABA問題引起的BUG
- 解決方案
CAS是什么
- CAS (compare and swap) 比較并交換,CAS 是物理層次支持程序的原子操作。說起原子性,這就設計到線程安全問題,在代碼的層面為了解決多線程并發處理同一共享資源造成的線程安全問題,我們常常會使用 synchronized 修飾代碼塊,變量等,將程序背后的指令封裝成原子性(事務的最小單位,不可再分),當一個線程執行 synchronized 修飾的代碼塊時獲取指定對象的對象鎖(一個對象只有一把鎖),其他并發處理同一代碼塊的線程因無法獲取對象鎖就會進入阻塞等待對象鎖釋放,然后繼續競爭對象鎖,此時 synchronized 修飾的代碼塊就具有原子性,具有互斥性,不可搶占性。
- CAS 是CPU 物理層次支持的原子操作(一條指令).
讀取內存數據(V),預期原值(A)和新值(B)。
如果內存位置的值與預期原值相等,就將新值(B)更新到內存中,替換掉原內存數據,
如果內存位置的值與預期原值不相等,處理器不會做任何操作,
CAS 對數據操作后會返回一個 boolean 值判斷是否成功。
- 以上指令集合,可以視為CPU 物理層次支持的一條指令,同樣可以解決多線程并發處理同一共享資源造成的線程安全問題。
CAS 使用Java偽代碼理解含義:
// address 原內存值
// expectValue 舊的預期值
// swapValue 需要修改的新值
// & 代表取地址,這里主要是理解這層含義,java 語法不支持
boolean CAS(address, expectValue, swapValue) {if (&address == expectedValue) {&address = swapValue;return true;}return false;
}
- 這么表示, 那么我們最終關心的是內存中的值, 至于寄存器的值一般都不要了
CAS的應用
- 操作系統會對 CPU 指令進行封裝,JVM 又會對操作系統提供的 API 再進行一層封裝,由于 CAS 本身就是 CPU 指令,所以在 Java 中也有關于 CAS 的 API,關于 CAS 的 API 放在了 unsafe 類里,Java 的標準庫中又對 CAS 進行了進一步的封裝,并且提供了一些工具類,可以讓我們直接使用。
實現原子類
- 原子類,就是 Java 標準庫對 CAS 進行進一步封裝后提供的一種工具類,如下圖所示:
- 在原子類中可以看到,它對 Integer 和 Long 進行了封裝,此時針對這樣的對象再進行多線程修改,就是線程安全的了,不知道大家還記不記得前面介紹線程安全時的一個代碼示例,就是利用兩個線程分別對同一個變量分別進行自增 50000 次,當時用這個代碼示例來演示了線程不安全的效果,后來我們通過加鎖的方式解決了這樣的問題,下面我就來使用原子類來寫一個用兩個線程對同一變量分別進行 50000 次自增的代碼,來看看此時會不會出現線程安全問題,代碼及運行結果如下所示:
public class demo36 {private static AtomicInteger count = new AtomicInteger();public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(()->{for(int i = 0; i < 50000; i++) {count.getAndIncrement(); //前綴自增//count.incrementAndGet(); //后綴自增}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {// count++;count.getAndIncrement();}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);}
}
-
如上圖所示,使用原子類不會出現之前的問題,這是因為之前使用的 result++ 是三個指令,在多線程中的三個指令會穿插執行,所以會引起線程不安全,此處的 getAndIncrement 對變量的修改是一個 CAS 指令,天生就是原子的,就不會出現穿插執行這種問題了,并且這個代碼不涉及加鎖,不需要阻塞等待,更高效。
- 那么上面的這段代碼是如何做到把自增操作變成原子的呢?我們可以進入源碼來嘗試找尋一下答案,如下圖所示:
- 層層點入到了native修飾的代碼, 表示是C++寫的代碼, 我們無法直接能看見. 這里我們用偽代碼來理解一下邏輯
// 下面代碼是一段偽代碼(不能編譯執行,只是用來表示邏輯)
class AtomicInteger {// value 表示我們要進行自增操作的變量private int value;public int getAndIncrement() {// oldValue 表示放到寄存器中的值int oldValue = value;// CAS 進行比較交換while (CAS(value, oldValue, oldValue + 1) != true) {// value 與 oldValue 的值不相等,說明在此期間有其他線程修改了 value 的值// 更新 oldValue 的值與 value 相等,再次進行循環oldValue = value;}// 當 value 與 oldValue 相等,就將 oldValue+1 的值賦值給 value// 以此來實現 value 的自增操作return Value;}
}
- 可以看到我們判斷和賦值都是一條指令, 這里線程的隨機調度并不能把判斷和賦值分開. 這就保證了我們這里比較和交換是原子的. 和我們不加鎖比較和交換指令被線程調度插隊不一樣, 也就保證了我們線程安全.
- 可以看到我們使用CAS后這個線程是允許t2線程指令插隊在t1線程指令前面的, 個時候他插隊了那么肯定內存的值和寄存器的值一定不一樣這, 那么我們就需要重新加載寄存器的值到內存中讓寄存器和內存中的值保證一樣. 這就和我們加鎖不讓t2線程插隊, 執行的結果一樣了.
- 為了確保當前讀取到的值是內存中最新值付出的代價就是“自旋”,如果不是最新值就不斷的循環判斷,直到是最新值后再進行修改,此時就會消耗更多的 CPU 資源。
實現自旋鎖
- 自旋鎖一般用于鎖競爭不激烈的情況下,在上述代碼中,當 owner 不為 null 的時候,循環就會一直執行,通過這樣的“忙等”來完成等待的效果,此處自旋式的等沒有放棄 CPU,不會參與到調度中,省去了調度開銷,但是會消耗更多 CPU 的資源。
ABA問題
ABA問題概述
- 圖中的場景就是t1線程想把num值改成100, 這個時候執行過程如下
- 先讀取 num 的值,記錄到 oldNum 變量中;
- 使用 CAS 判斷當前 num 的值是否為 0,如果為 0 就修改成 100。
- 但是在這個時候, t1線程修改到num之前, t2線程優先被調度了, t2線程把num修改到100后, 又修改為0 .這個時候t1線程希望改的num是沒有變過的, 結果t2改變了又改回去了. t1線程就無法判斷到底這個num值有沒有被改動過.這種問題就好比我們買了一款新手機,無法分辨這是由別人使用過重新翻新的還是他原本就是新的。
ABA問題引起的BUG
解決方案
- 我們從上面的例子中看見, ABA問題出現的核心原因就是另一個線程對同一個變量進行修改, 進行改動(如加操作), 然后又改回去(如減操作), 那么當前線程就并不知道他是進行了對這個變量進行改動(已經執行了減操作)的. 所以再去進行減操作.
- 為了解決這個問題. 我們約定不允許進行又進行加操作, 又進行減操作.這個時候另外一個線程對一個變量進行改動(如加操作), 就不可能還原原來變量的值(如減操作)對于本身就必須雙向變化的數據,可以引入一個版本號,版本號這個數字只能增加不能減少,此時就可以根據版本號來判斷當前數字是不是第一次被修改。
- 這個時候我們再進行上面的扣款操作
t1線程獲取到當前余額是1000, 版本號為1, 期望扣款500. 也就是把余額修改為500. 這個時候調度到t2線程
t2線程獲取當前余額是1000, 版本號為2, 余額修改為500(扣款成功). 調度到t3線程
t3線程獲取當前余額是500, 版本號為3, 想對余額里面轉賬500. 修改余額為1000. 調度到t1
t1線程當前余額為1000, 與之前獲取的余額相同. 但是當前版本號為3, 與之前的版本號1不同. 這個時候期望扣款500就失敗了.