簡(jiǎn)單了解JavaCAS的相關(guān)知識(shí)原理
JMM與問(wèn)題引入
為啥先說(shuō)JMM,因?yàn)镃AS的實(shí)現(xiàn)類中維護(hù)的變量都被volatile修飾, 這個(gè)volatile 是遵循JMM規(guī)范(不是百分百遵循,下文會(huì)說(shuō))實(shí)現(xiàn)的保證多線程并發(fā)訪問(wèn)某個(gè)變量實(shí)現(xiàn)線程安全的手段
一連串的知識(shí)點(diǎn)慢慢縷
首先說(shuō)什么是JMM, JMM就是大家所說(shuō)的java的內(nèi)存模型, 它是人們?cè)谶壿嬌献龀龅膭澐? 或者可以將JMM當(dāng)成是一種規(guī)范, 有哪些規(guī)范呢? 如下
- 可見(jiàn)性: 某一個(gè)線程對(duì)內(nèi)存中的變量做出改動(dòng)后,要求其他的線程在第一事件內(nèi)馬上馬得到通知,在CAS的實(shí)現(xiàn)中, 可見(jiàn)性其實(shí)是通過(guò)不斷的while循環(huán)讀取而得到的通知, 而不是被動(dòng)的得到通知
- 原子性: 線程在執(zhí)行某個(gè)操作的時(shí),要么一起成功,要么就一起失敗
- 有序性: 為了提高性能, 編譯器處理器會(huì)進(jìn)行指令的重排序, 源碼-> 編譯器優(yōu)化重排 -> 處理器優(yōu)化重排 -> 內(nèi)存系統(tǒng)重排 -> 最終執(zhí)行的命令
JVM運(yùn)行的實(shí)體是線程, 每一個(gè)線程在創(chuàng)建之后JVM都會(huì)為其創(chuàng)建一個(gè)工作空間, 這個(gè)工作空間是每一個(gè)線程之間的私有空間, 并且任何兩條線程之間的都不能直接訪問(wèn)到對(duì)方的工作空間, 線程之間的通信,必須通過(guò)共享空間來(lái)中轉(zhuǎn)完成
JMM規(guī)定所有的變量全部存在主內(nèi)存中,主內(nèi)存是一塊共享空間,那么如果某個(gè)線程相對(duì)主內(nèi)存中共享變量做出修改怎么辦呢? 像
下面這樣:
- 將共享變量的副本拷貝到工作空間中
- 對(duì)變量進(jìn)行賦值修改
- 將工作空間中的變量寫回到內(nèi)存中
JMM還規(guī)定如下:
- 任何線程在解鎖前必須將工作空間的共享變量立即刷新進(jìn)內(nèi)存中
- 線程在加鎖前必須讀取主內(nèi)存中的值更新到自己的工作空間中
- 加鎖和解鎖是同一把鎖
問(wèn)題引入
這時(shí)候如果多個(gè)線程并發(fā)按照上面的三步走去訪問(wèn)主內(nèi)存中的共享變量的話就會(huì)出現(xiàn)線程安全性的問(wèn)題, 比如說(shuō) 現(xiàn)在主內(nèi)存中的共享變量是c=1, 有AB兩個(gè)線程去并發(fā)訪問(wèn)這個(gè)c變量, 都想進(jìn)行c++, 現(xiàn)在A將c拷貝到自己的工作空間進(jìn)行c++, 于是c=2 , 于此同時(shí)線程B也進(jìn)行c++, c在B的工作空間中=2, AB線程將結(jié)果寫回工作空間最終的結(jié)果就是2, 而不是我們預(yù)期的3
相信怎么解決大家都知道, 就是使用JUC,中的原子類就能規(guī)避這個(gè)問(wèn)題
而原子類的底層實(shí)現(xiàn)使用的就是CAS技術(shù)
什么是CAS
CAS(compare and swap) 顧名思義: 比較和交換,在JUC中原子類的底層使用的都是CAS無(wú)鎖實(shí)現(xiàn)線程安全,是一門很炫的技術(shù)
如下面兩行代碼, 先比較再交換, 即: 如果從主內(nèi)存中讀取到的值為4就將它更新為2019
AtomicInteger atomicInteger = new AtomicInteger(4); atomicInteger.compareAndSet(4,2019);
跟進(jìn)AtomicInteger的源碼如下, 底層維護(hù)著一個(gè)int 類型的 變量, (當(dāng)然是因?yàn)槲疫x擇的原來(lái)類是AtomicInteger類型), 并且這個(gè)int類型的值被 volatile 修飾
private volatile int value; /** * Creates a new AtomicInteger with the given initial value. * * @param initialValue the initial value */ public AtomicInteger(int initialValue) { value = initialValue; }
什么是volatile
volatile是JVM提供的輕量的同步機(jī)制, 為什么是輕量界別呢? , 剛才在上面說(shuō)了JMM規(guī)范中提到了三條特性, 而JVM提供的volatile僅僅滿足上面的規(guī)范中的 2/3, 如下:
- 保證可見(jiàn)性
- 不保證原子性
- 禁止指令重排序
單獨(dú)的volatile是不能滿足原子性的,即如下代碼在多線程并發(fā)訪問(wèn)的情況下依然會(huì)出現(xiàn)線程安全性問(wèn)題
private volatile int value; public void add(){ value++; }
那么JUC的原子類是如何實(shí)現(xiàn)的 可以滿足原子性呢? 于是就不得不說(shuō)本片博文的主角, CAS
CAS源碼跟進(jìn)
我們跟進(jìn)AtomicInteger中的先遞增再獲取的方法 incrementAndGet()
public final int incrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, 1) + 1; }
通過(guò)代碼我們看到調(diào)用了Unsafe類來(lái)實(shí)現(xiàn)
什么是Unsafe類?
進(jìn)入U(xiǎn)nsafe類,可以看到他里面存在大量的 native方法,這些native方法全部是空方法,
這個(gè)unsafe類其實(shí)相當(dāng)于一個(gè)后門,他是java去訪問(wèn)調(diào)用系統(tǒng)上 C C++ 函數(shù)類庫(kù)的方法 如下圖
繼續(xù)跟進(jìn)這個(gè)方法incrementAndGet() 于是我們就來(lái)到了我們的主角方法, 關(guān)于這個(gè)方法倒是不難理解,主要是搞清楚方法中的var12345到底代表什么就行, 如下代碼+注釋
var1: 上一個(gè)方法傳遞進(jìn)來(lái)的: this,即當(dāng)前對(duì)象 var2: 上一個(gè)方法傳遞進(jìn)來(lái)的valueOffset, 就是內(nèi)存地址偏移量 通過(guò)這個(gè)內(nèi)存地址偏移量我能精確的找到要操作的變量在內(nèi)存中的地址 var4: 上一個(gè)方法傳遞進(jìn)來(lái)的1, 就是每次增長(zhǎng)的值 var5: 通過(guò)this和內(nèi)存地址偏移量讀取出來(lái)的當(dāng)前內(nèi)存中的目標(biāo)值 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; }
注意它用的是while循環(huán), 相對(duì)if(flag){} 這種寫法會(huì)多一次判斷, 整體的思路就是 在進(jìn)行修改之前先進(jìn)行一次比較,如果讀取到的當(dāng)前值和預(yù)期值是相同的,就自增,否則的話就繼續(xù)輪詢修改
小總結(jié)
通過(guò)上面的過(guò)程, 其實(shí)就能總結(jié)出CAS的底層實(shí)現(xiàn)原理
- volatile
- 自旋鎖
- unsafe類
補(bǔ)充: CAS通過(guò)Native方法的底層實(shí)現(xiàn),本質(zhì)上是操作系統(tǒng)層面上的CPU的并發(fā)原語(yǔ),JVM會(huì)直接實(shí)現(xiàn)出匯編層面的指令,依賴于硬件去實(shí)現(xiàn), 此外, 對(duì)于CPU的原語(yǔ)來(lái)說(shuō), 有兩條特性1,必定連續(xù), 2.不被中斷
CAS的優(yōu)缺點(diǎn)
優(yōu)點(diǎn):
它的底層我們看到了通過(guò)do-while 實(shí)現(xiàn)的自旋鎖來(lái)實(shí)現(xiàn), 就省去了在多個(gè)線程之間進(jìn)行切換所帶來(lái)的額外的上下文切換的開銷
缺點(diǎn):
- 通過(guò)while循環(huán)不斷的嘗試獲取, 省去了上下文切換的開銷,但是占用cpu的資源
- CAS只能保證一個(gè)共享變量的原子性, 如果存在多個(gè)共享變量的話不得不加鎖實(shí)現(xiàn)
- 存在ABA問(wèn)題
ABA問(wèn)題
什么是ABA問(wèn)題
我們這樣玩, 還是AB兩個(gè)線程, 給AtomicInteger賦初始值0
A線程中的代碼如下:
Thread.sleep(3000); atomicInteger.compareAndSet(0,2019);
B線程中的代碼如下:
atomicInteger.compareAndSet(0,1); atomicInteger.compareAndSet(1,0);
AB線程同時(shí)啟動(dòng), 雖然最終的結(jié)果A線程能成果的將值修改成2019,,但是它不能感知到在他睡眠過(guò)程中B線程對(duì)數(shù)據(jù)進(jìn)行過(guò)改變, 換句話說(shuō)就是A線程被B線程欺騙了
ABA問(wèn)題的解決--- AtomicStampedRefernce.java
帶時(shí)間戳的原子引用, 實(shí)現(xiàn)的機(jī)制就是通過(guò) 原子引用+版本號(hào)來(lái)完成, 每次對(duì)指定值的修改相應(yīng)的版本號(hào)會(huì)加1, 實(shí)例如下
// 0表示初始化, 1表示初始版本號(hào) AtomicStampedReference<Integer> reference = new AtomicStampedReference<>(0, 1); reference.getStamp(); // 獲取版本號(hào) reference.attemptStamp(1,2); // 期待是1, 如果是1就更新為2
原子引用
JUC中我們可以找到像AtomicInteger這樣已經(jīng)定義好了實(shí)現(xiàn)類, 但是JUC沒(méi)有給我們提供類似這樣 AtomicUser或者 AtomicProduct 這樣自定義類型的原子引用類型啊, 不過(guò)java仍然是提供了后門就是 原子引用類型
使用實(shí)例:
User user = getUserById(1); AtomicReference<User> userAtomicReference = new AtomicReference<User>(); user.setUsername("張三"); userAtomicReference.compareAndSet(user,user);
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
詳解Spring boot使用Redis集群替換mybatis二級(jí)緩存
本篇文章主要介紹了詳解Spring boot使用Redis集群替換mybatis二級(jí)緩存,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-05-05SpringBoot+MyBatisPlus對(duì)Map中Date格式轉(zhuǎn)換處理的方法詳解
在?SpringBoot?項(xiàng)目中,?如何統(tǒng)一?JSON?格式化中的日期格式。本文將為大家介紹一種方法:利用MyBatisPlus實(shí)現(xiàn)對(duì)Map中Date格式轉(zhuǎn)換處理,需要的可以參考一下2022-10-10SSH框架網(wǎng)上商城項(xiàng)目第18戰(zhàn)之過(guò)濾器實(shí)現(xiàn)購(gòu)物登錄功能的判斷
這篇文章主要為大家詳細(xì)介紹了SSH框架網(wǎng)上商城項(xiàng)目第18戰(zhàn):過(guò)濾器實(shí)現(xiàn)購(gòu)物登錄功能的判斷,感興趣的小伙伴們可以參考一下2016-06-06Java?IO模型之BIO、NIO、AIO三種常見(jiàn)IO模型解析
這篇文章主要介紹了今天我們來(lái)聊Java?IO模型,BIO、NIO、AIO三種常見(jiàn)IO模型,我們從應(yīng)用調(diào)用的過(guò)程中來(lái)分析一下整個(gè)IO的執(zhí)行過(guò)程,不過(guò)在此之前,我們需要簡(jiǎn)單的了解一下整個(gè)操作系統(tǒng)的空間布局,需要的朋友可以參考下2024-07-07Spring Security使用單點(diǎn)登錄的權(quán)限功能
本文主要介紹了Spring Security使用單點(diǎn)登錄的權(quán)限功能,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-04-04java easyUI實(shí)現(xiàn)自定義網(wǎng)格視圖實(shí)例代碼
這篇文章主要給大家介紹了關(guān)于java easyUI實(shí)現(xiàn)自定義網(wǎng)格視圖的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2018-10-10在CentOS系統(tǒng)中檢測(cè)Java安裝及運(yùn)行jar應(yīng)用的方法
這篇文章主要介紹了在CentOS系統(tǒng)中檢測(cè)Java安裝及運(yùn)行jar應(yīng)用的方法,同樣適用于Fedora等其他RedHat系的Linux系統(tǒng),需要的朋友可以參考下2015-06-06Kafka消費(fèi)客戶端協(xié)調(diào)器GroupCoordinator詳解
這篇文章主要為大家介紹了Kafka消費(fèi)客戶端協(xié)調(diào)器GroupCoordinator使用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10