悟空
種樹比較好的時間是十年前,其次是如今。
自主開發了Java學習平臺、PMP刷題小程序。目前主修Java
、多線程
、SpringBoot
、SpringCloud
、k8s
。
本公衆號不限於分享技術,也會分享工具的使用、人生感悟、讀書總結。java
夜黑風高的晚上,一名苦逼程序員正在瘋狂敲着鍵盤,忽然他老婆帶着一副睡眼朦朧的眼神瞟了下電腦桌面。因而有了以下對話:git
老婆:這畫的圖是啥意思,怎麼還有三角形,四邊形?程序員
我:我在畫CAS的原理,要不我跟你講一遍?github
老婆:好呀!小程序
案例:甲看見一個三角形積木,以爲很差看,想替換成五邊形,可是乙想把積木替換成四邊形。(前提條件,只能被替換一次)安全
甲比較雞賊,想到了一個辦法:「我把積木帶到另一個房間裏面去替換,並上鎖,就不會被別人打擾了。」(這裏用到了排他鎖synchronized
)markdown
乙以爲甲太不厚道:「房間上了鎖,我進不去,我也看不見積木長啥樣。(因上了鎖,因此不能訪問)」多線程
因而甲、乙想到了另一個辦法:誰先搶到積木,誰先替換,若是積木形狀變了,則不容許其餘人再次替換。(比較並替換CAS
)架構
因而他們就開始搶三角形積木:併發
場景1:甲搶到,替換成五邊形,乙不能替換
乙後搶到,積木已經變爲五邊形了,乙就沒機會替換了(由於甲、乙共一次替換機會)。
場景2:乙搶到未替換,甲替換成功
假如乙先搶到了,可是忽然以爲三角形也挺好看的,沒有替換,放下積木就走開了。
而後甲搶到了積木,積木仍是三角形的,想到乙沒有替換,就把三角形替換成五邊形了。
場景3:乙搶到,替換成三角形,甲替換成五邊形,ABA問題
老婆聽完後,以爲這三種場景都太簡單了,原來計算機這麼簡單,早知道我也去學計算機。。。
被無情鄙視了,好在老婆竟然聽懂了,不知道你們聽懂沒?
迴歸正傳,咱們用計算機術語來說下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
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
複製代碼
咱們來對比看下原理圖理解下上面代碼的過程
圖畫得很是棒!
上述的場景和咱們用Git代碼管理工具是同樣的,若是有人先提交了代碼到develop分支,另一我的想要改這個地方的代碼,就得先pull develop分支,以避免提交時提示衝突。
這裏咱們用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
還未執行自增操做。
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;
}
複製代碼
源碼解釋: 劃重點!!!
this.getIntVolatile(var1, var2)
:根據當前對象var1和對象的內存偏移量var2獲得主內存中變量的值,賦值給var5,並在main線程的工做內存中存放一份var5的副本(4)在362行打上斷點,main線程繼續執行一步
(5)切換到子線程aaa,仍是在361行斷點處,還未獲取主內存的值
(6)子線程aaa繼續執行一步,獲取到var5的值等於10
(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。
(8)切換到子線程aaa,進行比較並自增
由於此時aaa線程的var5=10,而主內存中的值已經更新爲11了,因此比較後發現被其餘線程修改了,不能進行替換,返回false,繼續執行do while循環。
(10)子線程aaa繼續執行,進行比較和替換,結果爲true
因var5=11,主內存中的變量值也等於11,因此比較後相等,能夠進行替換,替換值爲var5+var4,結果爲12,也就是自增1。退出循環,返回變量更新前的值var5=11。
至此,getAndIncrement方法的整個原子自增的邏輯就debug完了。因此能夠得出結論:
先比較線程中的副本是否與主內存相等,相等則能夠進行自增,並返回副本的值,若其餘線程修改了主內存中的值,當前線程不能進行自增,須要從新獲取主內存的值,而後再次判斷是否與主內存中的值是否相等,以此往復。
不知道你們發現沒,aaa線程可能會出現循環屢次的問題,由於其餘線程可能將主內存的值又改了,可是aaa線程拿到的仍是老的數據,就會出現再循環一次,就會給CPU帶來性能開銷。這個就是自旋
。
頻繁出現自旋,循環時間長,開銷大
(由於執行的是do while,若是比較不成功一直在循環,最差的狀況,就是某個線程一直取到的值和預期值都不同,這樣就會無限循環)一個
共享變量的原子操做
一個
共享變量執行操做時,咱們能夠經過循環CAS的方式來保證原子操做多個
共享變量操做時,循環CAS就沒法保證操做的原子性,這個時候只能用鎖來保證原子性本篇從和老婆的對話開始,以通俗的語言給老婆講了CAS問題,其中還涉及到了併發鎖。而後從底層代碼一步一步debug,深刻理解了CAS的原理。
每一張圖都力求精美!分享+在看啊,大佬們!
彩蛋: 還有一個ABA問題沒有給你們講,另外這裏怎麼不是AAB(拖拉機),AAA(金花)?
這周前三天寫技術文章花了大量時間,少熬夜,睡覺啦 ~ 咱們下期再來說ABA問題,小夥伴們分享轉發下好嗎?您的支持是我寫做最大的動力~
悟空,一隻努力變強的碼農!我要變身超級賽亞人啦!
另外能夠搜索「悟空聊架構」或者PassJava666,一塊兒進步!