程序員深夜慘遭老婆鄙視,緣由竟是CAS原理太簡單?| 每一張圖都力求精美

mark

悟空
種樹比較好的時間是十年前,其次是如今。
自主開發了Java學習平臺、PMP刷題小程序。目前主修Java多線程SpringBootSpringCloudk8s
本公衆號不限於分享技術,也會分享工具的使用、人生感悟、讀書總結。java

夜黑風高的晚上,一名苦逼程序員正在瘋狂敲着鍵盤,忽然他老婆帶着一副睡眼朦朧的眼神瞟了下電腦桌面。因而有了以下對話:git

老婆:這畫的圖是啥意思,怎麼還有三角形,四邊形?程序員

我:我在畫CAS的原理,要不我跟你講一遍?github

老婆:好呀!小程序

請開始你的表演

案例:甲看見一個三角形積木,以爲很差看,想替換成五邊形,可是乙想把積木替換成四邊形。(前提條件,只能被替換一次)安全

案例

甲比較雞賊,想到了一個辦法:「我把積木帶到另一個房間裏面去替換,並上鎖,就不會被別人打擾了。」(這裏用到了排他鎖synchronizedmarkdown

乙以爲甲太不厚道:「房間上了鎖,我進不去,我也看不見積木長啥樣。(因上了鎖,因此不能訪問)」多線程

甲把房間鎖住了

因而甲、乙想到了另一個辦法:誰先搶到積木,誰先替換,若是積木形狀變了,則不容許其餘人再次替換。(比較並替換CAS架構

因而他們就開始搶三角形積木:併發

  • 場景1:甲搶到,替換成五邊形,乙不能替換

    • 假如甲先搶到了,積木仍是三角形的,就把三角形替換成五邊形了。

    甲先搶到,替換成五邊形

    • 乙後搶到,積木已經變爲五邊形了,乙就沒機會替換了(由於甲、乙共一次替換機會)。

      mark

  • 場景2:乙搶到未替換,甲替換成功

    • 假如乙先搶到了,可是忽然以爲三角形也挺好看的,沒有替換,放下積木就走開了。

    • 而後甲搶到了積木,積木仍是三角形的,想到乙沒有替換,就把三角形替換成五邊形了。

    乙搶到未替換,甲替換成功

  • 場景3:乙搶到,替換成三角形,甲替換成五邊形,ABA問題

    • 假如乙先搶到了,可是以爲這個三角形是舊的,就換了另一個一摸同樣的三角形,只是積木比較新。
    • 而後甲搶到了積木,積木仍是三角形的,想到乙沒有替換,就把三角形替換成五邊形了。

乙搶到,替換成三角形,甲替換成五邊形,ABA問題

老婆聽完後,以爲這三種場景都太簡單了,原來計算機這麼簡單,早知道我也去學計算機。。。

mark

被無情鄙視了,好在老婆竟然聽懂了,不知道你們聽懂沒?

迴歸正傳,咱們用計算機術語來說下Java CAS的原理

1、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是線程安全的。

2、能寫幾行代碼說明下嗎?

在上篇講volatile時,講到了如何使用原子整型類AtomicInteger來解決volatile的非原子性問題,保證多個線程執行num++的操做,最終執行的結果與單線程一致,輸出結果爲20000。

此次咱們仍是用AtomicInteger。

首先定義atomicInteger變量的初始值等於10,主內存中的值設置爲10

AtomicInteger atomicInteger = new AtomicInteger(10);
複製代碼

而後調用atomicInteger的CAS方法,先比較當前變量atomicInteger的值是不是10,若是是,則將變量的值設置爲20

atomicInteger.compareAndSet(10, 20);
複製代碼

設置成功,atomicInteger更新爲20

當咱們再次調用atomicInteger的CAS方法,先比較當前變量atomicInteger的值是不是10,若是是,則將變量的值設置爲30

atomicInteger.compareAndSet(10, 30);
複製代碼

設置失敗,因atomicInteger的當前值爲20,而比較值是10,因此比較後,不相等,故不能進行更新

完整代碼以下:

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);
    }
}
複製代碼

執行結果以下:

當前atomicInteger變量的值:20 比較結果true
當前atomicInteger變量的值:20, 比較結果false
複製代碼

atomicInteger比較並交換的示例結果

咱們來對比看下原理圖理解下上面代碼的過程

  • 第一步:線程1和線程2都有主內存中變量的拷貝,值都等於10

mark

  • 第二步:線程1想要將值更新爲20,先要將工做內存中的變量值與主內存中的變量進行比較,值都等於10,因此能夠將主內存中的值替換成20

mark

  • 第三步:線程1將主內存中的值替換成20,並將線程1中的工做內存中的副本更新爲20

mark

  • 第四步:線程2想要將變量更新爲30,先要將線程2的工做內存中的值與主內存進行比較10不等於20,因此不能更新

mark

  • 第五步:線程2將工做內存的副本更新爲與主內存一致:20

mark

圖畫得很是棒!

mark

上述的場景和咱們用Git代碼管理工具是同樣的,若是有人先提交了代碼到develop分支,另一我的想要改這個地方的代碼,就得先pull develop分支,以避免提交時提示衝突。

3、能講下CAS底層原理嗎?

源碼調試

這裏咱們用atomicInteger的getAndIncrement()方法來說解,這個方法裏面涉及到了比較並替換的原理。

示例以下:

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

getAndIncrement方法會調用unsafe的getAndAddInt方法,

public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}
複製代碼
  • (3)在源碼getAndAddInt方法的361行打上斷點,main線程先執行到361行

    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

  • (4)在362行打上斷點,main線程繼續執行一步

    • var5獲取到主內存中的值爲10

    2

  • (5)切換到子線程aaa,仍是在361行斷點處,還未獲取主內存的值

    3

  • (6)子線程aaa繼續執行一步,獲取到var5的值等於10

4

(7)切換到main線程,進行比較並替換

this.compareAndSwapInt(var1, var2, var5, var5 + var4)
複製代碼

var5=10,經過var1和var2獲取到的值也是10,由於沒有其餘線程修改變量。compareAndSwapInt的源碼咱們後面再說。

因此比較後,發現變量沒被其餘線程修改,能夠進行替換,替換值爲var5+var4=11,變量值替換後爲 11,也就是自增1。這行代碼執行結果返回true(自增成功),退出do while循環。return值爲變量更新前的值10。

5

(8)切換到子線程aaa,進行比較並自增

由於此時aaa線程的var5=10,而主內存中的值已經更新爲11了,因此比較後發現被其餘線程修改了,不能進行替換,返回false,繼續執行do while循環。

6

  • (9)子線程aaa繼續執行,從新獲取到的var=11

7

  • (10)子線程aaa繼續執行,進行比較和替換,結果爲true

    因var5=11,主內存中的變量值也等於11,因此比較後相等,能夠進行替換,替換值爲var5+var4,結果爲12,也就是自增1。退出循環,返回變量更新前的值var5=11。

8

至此,getAndIncrement方法的整個原子自增的邏輯就debug完了。因此能夠得出結論:

先比較線程中的副本是否與主內存相等,相等則能夠進行自增,並返回副本的值,若其餘線程修改了主內存中的值,當前線程不能進行自增,須要從新獲取主內存的值,而後再次判斷是否與主內存中的值是否相等,以此往復。

4、CAS有什麼問題?

不知道你們發現沒,aaa線程可能會出現循環屢次的問題,由於其餘線程可能將主內存的值又改了,可是aaa線程拿到的仍是老的數據,就會出現再循環一次,就會給CPU帶來性能開銷。這個就是自旋

  • 頻繁出現自旋,循環時間長,開銷大(由於執行的是do while,若是比較不成功一直在循環,最差的狀況,就是某個線程一直取到的值和預期值都不同,這樣就會無限循環)
  • 只能保證一個共享變量的原子操做
    • 當對一個共享變量執行操做時,咱們能夠經過循環CAS的方式來保證原子操做
    • 可是對於多個共享變量操做時,循環CAS就沒法保證操做的原子性,這個時候只能用鎖來保證原子性
  • 引出來ABA問題(有彩蛋)

5、小結

本篇從和老婆的對話開始,以通俗的語言給老婆講了CAS問題,其中還涉及到了併發鎖。而後從底層代碼一步一步debug,深刻理解了CAS的原理。

每一張圖都力求精美!分享+在看啊,大佬們!

彩蛋: 還有一個ABA問題沒有給你們講,另外這裏怎麼不是AAB(拖拉機),AAA(金花)?

4個A

這周前三天寫技術文章花了大量時間,少熬夜,睡覺啦 ~ 咱們下期再來說ABA問題,小夥伴們分享轉發下好嗎?您的支持是我寫做最大的動力~

悟空,一隻努力變強的碼農!我要變身超級賽亞人啦!

悟空

另外能夠搜索「悟空聊架構」或者PassJava666,一塊兒進步!

個人GitHub主頁,關注個人Spring Cloud 實戰項目《佳必過》

相關文章
相關標籤/搜索