1. 程式人生 > >程式設計師深夜慘遭老婆鄙視,原因竟是CAS原理太簡單?| 每一張圖都力求精美

程式設計師深夜慘遭老婆鄙視,原因竟是CAS原理太簡單?| 每一張圖都力求精美

![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)