程式設計師深夜慘遭老婆鄙視,原因竟是CAS原理太簡單?| 每一張圖都力求精美
阿新 • • 發佈:2020-08-24
![mark](http://cdn.jayh.club/blog/20200822/EKqCyvLNh70T.png?imageslim)
> `悟空`
> 種樹比較好的時間是十年前,其次是現在。
> 自主開發了Java學習平臺、PMP刷題小程式。目前主修`Java`、`多執行緒`、`SpringBoot`、`SpringCloud`、`k8s`。
> 本公眾號不限於分享技術,也會分享工具的使用、人生感悟、讀書總結。
夜黑風高的晚上,一名苦逼程式設計師正在瘋狂敲著鍵盤,突然他老婆帶著一副睡眼朦朧的眼神瞟了下電腦桌面。於是有了如下對話:
>老婆:這畫的圖是啥意思,怎麼還有三角形,四邊形?
>
>我:我在畫CAS的原理,要不我跟你講一遍?
>
>老婆:好呀!
![請開始你的表演](http://cdn.jayh.club/blog/20200819/QrgY3SbBks2v.png?imageslim)
**案例:甲看見一個三角形積木,覺得不好看,想替換成五邊形,但是乙想把積木替換成四邊形。(前提條件,只能被替換一次)**
![案例](http://cdn.jayh.club/blog/20200818/OEvyi3KQE95G.png?imageslim)
甲比較雞賊,想到了一個辦法:“我把積木帶到另外一個房間裡面去替換,並上鎖,就不會被別人打擾了。”(這裡用到了`排他鎖synchronized`)
乙覺得甲太不厚道:“房間上了鎖,我進不去,我也看不見積木長啥樣。(因上了鎖,所以不能訪問)”
![甲把房間鎖住了](http://cdn.jayh.club/blog/20200818/zp0QVrLMySPu.png?imageslim)
於是甲、乙想到了另外一個辦法:**誰先搶到積木,誰先替換,如果積木形狀變了,則不允許其他人再次替換**。(`比較並替換CAS`)
於是他們就開始搶三角形積木:
- 場景1:`甲搶到,替換成五邊形,乙不能替換`
- 假如甲先搶到了,積木還是三角形的,就把三角形替換成五邊形了。
![甲先搶到,替換成五邊形](http://cdn.jayh.club/blog/20200819/PrE1uNFiib8y.png?imageslim)
- 乙後搶到,積木已經變為五邊形了,乙就沒機會替換了(因為甲、乙共一次替換機會)。
![mark](http://cdn.jayh.club/blog/20200818/leIc0y5ne6sO.png?imageslim)
- 場景2:`乙搶到未替換,甲替換成功`
- 假如乙先搶到了,但是突然覺得三角形也挺好看的,沒有替換,放下積木就走開了。
- 然後甲搶到了積木,積木還是三角形的,想到乙沒有替換,就把三角形替換成五邊形了。
![乙搶到未替換,甲替換成功](http://cdn.jayh.club/blog/20200818/HzprJycWtrux.png?imageslim)
- 場景3:`乙搶到,替換成三角形,甲替換成五邊形,ABA問題`
- 假如乙先搶到了,但是覺得這個三角形是舊的,就換了另外一個一摸一樣的三角形,只是積木比較新。
- 然後甲搶到了積木,積木還是三角形的,想到乙沒有替換,就把三角形替換成五邊形了。
![乙搶到,替換成三角形,甲替換成五邊形,ABA問題](http://cdn.jayh.club/blog/20200819/hC9pmBC6olW7.png?imageslim)
老婆聽完後,覺得這三種場景都太簡單了,**原來計算機這麼簡單,早知道我也去學計算機**。。。
![mark](http://cdn.jayh.club/blog/20200819/OuWkYsoSzQMe.png?imageslim)
被無情鄙視了,好在老婆居然聽懂了,不知道大家聽懂沒?
迴歸正傳,我們用計算機術語來講下Java CAS的原理
# 一、Java CAS簡介
**CAS的全稱:**Compare-And-Swap(比較並交換)。比較變數的現在值與之前的值是否一致,若一致則替換,否則不替換。
**CAS的作用:**原子性更新變數值,保證執行緒安全。
**CAS指令:**需要有三個運算元,變數的當前值(V),舊的預期值(A),準備設定的新值(B)。
**CAS指令執行條件:**當且僅當V=A時,處理器才會設定V=B,否則不執行更新。
**CAS的返回指:**V的之前值。
**CAS處理過程:**原子操作,執行期間不會被其他執行緒中斷,執行緒安全。
**CAS併發原語:**體現在Java語言中sun.misc.Unsafe類的各個方法。呼叫UnSafe類中的CAS方法,JVM會幫我們實現出CAS彙編指令,這是一種完全依賴於硬體的功能,通過它實現了原子操作。由於CAS是一種系統原語,原語屬於作業系統用於範疇,是由`若干條指令`組成,用於完成某個功能的一個過程,並且原語的執行必須是連續的,在執行過程中不允許被中斷,所以CAS是一條CPU的原子指令,不會造成所謂的資料不一致的問題,所以CAS是執行緒安全的。
# 二、能寫幾行程式碼說明下嗎?
在上篇講volatile時,講到了如何使用原子整型類AtomicInteger來解決volatile的非原子性問題,保證多個執行緒執行num++的操作,最終執行的結果與單執行緒一致,輸出結果為20000。
這次我們還是用AtomicInteger。
首先定義atomicInteger變數的初始值等於10,主記憶體中的值設定為10
```java
AtomicInteger atomicInteger = new AtomicInteger(10);
```
然後呼叫atomicInteger的CAS方法,先比較當前變數atomicInteger的值是否是10,如果是,則將變數的值設定為20
```java
atomicInteger.compareAndSet(10, 20);
```
設定成功,atomicInteger更新為20
當我們再次呼叫atomicInteger的CAS方法,先比較當前變數atomicInteger的值是否是10,如果是,則將變數的值設定為30
```java
atomicInteger.compareAndSet(10, 30);
```
設定失敗,因atomicInteger的當前值為20,而比較值是10,**所以比較後,不相等,故不能進行更新**。
完整程式碼如下:
```java
package com.jackson0714.passjava.threads;
import java.util.concurrent.atomic.AtomicInteger;
/**
演示CAS compareAndSet 比較並交換
* @author: 悟空聊架構
* @create: 2020-08-17
*/
public class CASDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(10);
Boolean result1 = atomicInteger.compareAndSet(10,20);
System.out.printf("當前atomicInteger變數的值:%d 比較結果%s\r\n", atomicInteger.get(), result1);
Boolean result2 = atomicInteger.compareAndSet(10,30);
System.out.printf("當前atomicInteger變數的值:%d, 比較結果%s\n" , atomicInteger.get(), result2);
}
}
```
執行結果如下:
``` java
當前atomicInteger變數的值:20 比較結果true
當前atomicInteger變數的值:20, 比較結果false
```
![atomicInteger比較並交換的示例結果](http://cdn.jayh.club/blog/20200818/0bMPvFoWDkew.png?imageslim)
我們來對比看下原理圖理解下上面程式碼的過程
- 第一步:執行緒1和執行緒2都有主記憶體中變數的拷貝,值都等於10
![mark](http://cdn.jayh.club/blog/20200819/XNCblvAWCr3w.png?imageslim)
- 第二步:執行緒1想要將值更新為20,先要將工作記憶體中的變數值與主記憶體中的變數進行比較,值都等於10,所以可以將主記憶體中的值替換成20
![mark](http://cdn.jayh.club/blog/20200819/lqJdYaKuklVl.png?imageslim)
- 第三步:執行緒1將主記憶體中的值替換成20,並將執行緒1中的工作記憶體中的副本更新為20
![mark](http://cdn.jayh.club/blog/20200819/i8Fv62wVF14v.png?imageslim)
- 第四步:執行緒2想要將變數更新為30,先要將執行緒2的工作記憶體中的值與主記憶體進行比較10不等於20,所以不能更新
![mark](http://cdn.jayh.club/blog/20200819/rpb0rUGrrHJk.png?imageslim)
- 第五步:執行緒2將工作記憶體的副本更新為與主記憶體一致:20
![mark](http://cdn.jayh.club/blog/20200819/11Np54tAbYk5.png?imageslim)
圖畫得非常棒!
![mark](http://cdn.jayh.club/blog/20200819/lvuvw7hiOURc.png?imageslim)
上述的場景和我們用Git程式碼管理工具是一樣的,如果有人先提交了程式碼到develop分支,另外一個人想要改這個地方的程式碼,就得先pull develop分支,以擴音交時提示衝突。
# 三、能講下CAS底層原理嗎?
## 原始碼除錯
這裡我們用atomicInteger的getAndIncrement()方法來講解,這個方法裡面涉及到了比較並替換的原理。
示例如下:
```java
public static void main(String[] args) throws InterruptedException {
AtomicInteger atomicInteger = new AtomicInteger(10);
Thread.sleep(100);
new Thread(() -> {
atomicInteger.getAndIncrement();
}, "aaa").start();
atomicInteger.getAndIncrement();
}
```
- (1)首先需要開啟IDEA的多執行緒除錯模式
- (2)我們先打斷點到17行,main執行緒執行到此行,子執行緒`aaa`還未執行自增操作。
![mark](http://cdn.jayh.club/blog/20200820/AQswHVY8bOw9.png?imageslim)
getAndIncrement方法會呼叫unsafe的`getAndAddInt`方法,
```java
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
```
- (3)在原始碼`getAndAddInt`方法的361行打上斷點,main執行緒先執行到361行
```java
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
```
**原始碼解釋:** *劃重點!!!*
- var1:當前物件,我們定義的atomicInteger
- var2:當前物件的記憶體偏移量
- var4:當前自增多少,預設為1,且不可設為其他值
- var5:當前變數的值
- `this.getIntVolatile(var1, var2)`:根據當前物件var1和物件的記憶體偏移量var2得到主記憶體中變數的值,賦值給var5,並在main執行緒的工作記憶體中存放一份var5的副本
![1](http://cdn.jayh.club/blog/20200820/w4Pbmh2KKIBu.png?imageslim)
- (4)在362行打上斷點,main執行緒繼續執行一步
- var5獲取到主記憶體中的值為10
![2](http://cdn.jayh.club/blog/20200820/Gt3sSVnLxYBa.png?imageslim)
- (5)切換到子執行緒aaa,還是在361行斷點處,還未獲取主記憶體的值
![3](http://cdn.jayh.club/blog/20200820/SecJqDjHaLe9.png?imageslim)
- (6)子執行緒aaa繼續執行一步,獲取到var5的值等於10
![4](http://cdn.jayh.club/blog/20200820/vdvPqeVu9sGF.png?imageslim)
(7)切換到main執行緒,進行比較並替換
```java
this.compareAndSwapInt(var1, var2, var5, var5 + var4)
```
var5=10,通過var1和var2獲取到的值也是10,因為沒有其他執行緒修改變數。compareAndSwapInt的原始碼我們後面再說。
所以比較後,發現變數沒被其他執行緒修改,可以進行替換,替換值為var5+var4=11,變數值替換後為 11,也就是自增1。這行程式碼執行結果返回true(自增成功),退出do while迴圈。return值為變數更新前的值10。
![5](http://cdn.jayh.club/blog/20200820/mqLNonLDmpm5.png?imageslim)
(8)切換到子執行緒aaa,進行比較並自增
因為此時aaa執行緒的var5=10,而主記憶體中的值已經更新為11了,所以比較後發現被其他執行緒修改了,不能進行替換,返回false,繼續執行do while迴圈。
![6](http://cdn.jayh.club/blog/20200820/5jzAQ3Xy22eL.png?imageslim)
- (9)子執行緒aaa繼續執行,重新獲取到的var=11
![7](http://cdn.jayh.club/blog/20200820/yqEqKOH3JKFl.png?imageslim)
- (10)子執行緒aaa繼續執行,進行比較和替換,結果為true
因var5=11,主記憶體中的變數值也等於11,所以比較後相等,可以進行替換,替換值為var5+var4,結果為12,也就是自增1。退出迴圈,返回變數更新前的值var5=11。
![8](http://cdn.jayh.club/blog/20200820/zx1kGeVHR834.png?imageslim)
至此,getAndIncrement方法的整個原子自增的邏輯就debug完了。所以可以得出結論:
> 先比較執行緒中的副本是否與主記憶體相等,相等則可以進行自增,並返回副本的值,若其他執行緒修改了主記憶體中的值,當前執行緒不能進行自增,需要重新獲取主記憶體的值,然後再次判斷是否與主記憶體中的值是否相等,以此往復。
# 四、CAS有什麼問題?
不知道大家發現沒,aaa執行緒可能會出現迴圈多次的問題,因為其他執行緒可能將主記憶體的值又改了,但是aaa執行緒拿到的還是老的資料,就會出現再迴圈一次,就會給CPU帶來效能開銷。這個就是`自旋`。
- `頻繁出現自旋,迴圈時間長,開銷大`(因為執行的是do while,如果比較不成功一直在迴圈,最差的情況,就是某個執行緒一直取到的值和預期值都不一樣,這樣就會無限迴圈)
- 只能保證`一個`共享變數的原子操作
- 當對`一個`共享變數執行操作時,我們可以通過迴圈CAS的方式來保證原子操作
- 但是對於`多個`共享變數操作時,迴圈CAS就無法保證操作的原子性,這個時候只能用鎖來保證原子性
- 引出來ABA問題(有彩蛋)
# 五、小結
本篇從和老婆的對話開始,以通俗的語言給老婆講了CAS問題,其中還涉及到了併發鎖。然後從底層程式碼一步一步debug,深入理解了CAS的原理。
每一張圖都力求精美!分享+在看啊,大佬們!
**彩蛋:**還有一個ABA問題沒有給大家講,另外這裡怎麼不是AAB(拖拉機),AAA(金花)?
![4個A](http://cdn.jayh.club/blog/20200821/b5JUIpeSuH7E.png?imageslim)
這周前三天寫技術文章花了大量時間,少熬夜,睡覺啦 ~ 我們下期再來講ABA問題,小夥伴們分享轉發下好嗎?您的支援是我寫作最大的動力~
悟空,一隻努力變強的碼農!我要變身超級賽亞人啦!
![悟空](http://cdn.jayh.club/blog/20200821/wp5kwn5UEWaJ.png?imageslim)
![公眾號](http://cdn.jayh.club/blog/20200824/085127482.png)