CAS 原子操作
點擊上方藍(lán)色字體,選擇“標(biāo)星公眾號”
優(yōu)質(zhì)文章,第一時間送達(dá)
? 作者?|??彼岸舞?
來源 |? urlify.cn/MRn6rm
理會CAS和CAS:
有時候面試官面試問你的時候,會問,談?wù)勀銓AS的理解,這時應(yīng)該有很多人,就會比較懵,當(dāng)然,我也會比較懵,當(dāng)然我和很多人的懵不同,很多人可能,并不知道CAS是一個什么東西,而在我看來我是不知道他問的是那個CAS
我一般會問面試官,問他問的CAS是"原子操作",還是"單點登錄"
因為在JAVA并發(fā)中的原子操作是稱為CAS的,也就是英文單詞CompareAndSwap的縮寫,中文意思是:比較并替換。
但是在企業(yè)應(yīng)用中CAS也被稱為企業(yè)級開源單點登錄解決方案,是 Central Authentication Service 的縮寫 —— 中央認(rèn)證服務(wù),一種獨立開放指令協(xié)議,是 Yale 大學(xué)發(fā)起的一個企業(yè)級開源項目,旨在為 Web 應(yīng)用系統(tǒng)提供一種可靠的 SSO 解決方案。
CAS(Compare And Swap):
我們先要學(xué)習(xí)的是并發(fā)編程中的CAS,也就是原子操作
那么,什么是原子操作?如何實現(xiàn)原子操作?
什么是原子操作:
原子,也是最小單位,是一個不可再分割的單位,不可被中斷的一個或者一系列操作
CAS是以一種無鎖的方式實現(xiàn)并發(fā)控制,在實際情況下,同時操作一個對象的概率非常小,所以多數(shù)加鎖操作做的基本是無用功
CAS以一種樂觀鎖的方式實現(xiàn)并發(fā)控制
如何實現(xiàn)原子操作:
Java可以通過鎖和循環(huán)CAS的方式實現(xiàn)原子操作
為什么要有CAS:
CAS就是比較并且替換的一個原子操作,在CPU的指令級別上進(jìn)行保證
為什么要有CAS:
Sync是基于阻塞的鎖的機制,
1:被阻塞的線程優(yōu)先級很高
2:拿到鎖的線程一直不釋放鎖則么辦
3:大量的競爭,消耗CPU,同時帶來死鎖或者其他線程安全
因為通過鎖實現(xiàn)原子操作時,其他線程必須等待已經(jīng)獲得鎖的線程運行完車之后才能獲取鎖,這樣就會占用系統(tǒng)大量資源
CAS原理:
從CPU指令級別保證這是一個原子操作
CAS包含哪些參數(shù):
三個運算符:
一個內(nèi)存地址V
一個期望的值A(chǔ)
一個新值B
基本思路:
如果地址V上的值和期望的值A(chǔ)相等,就給地址V賦值新值B,如果不是,不做任何操作
循環(huán)CAS:
在一個(死)循環(huán)中[for(;;)]里不斷進(jìn)行CAS操作,直到成功為止(自旋操作即死循環(huán))
CAS問題:
ABA問題:
那么什么是ABA問題?就是內(nèi)存中原本是A,然后通過CAS變成了B,然后再次通過CAS變成了A,這個過程中,相對于結(jié)果來說,是沒有任何改變的,但是相對于內(nèi)存來說,至少發(fā)生過兩次變化,這就是ABA問題
生活中:
就像你接了一杯水,這時水是滿的,但是這個時候,你的同時很渴,過來拿你的水直接喝掉了一半,這時水剩下了一半,接著,你的同事又重新把你的水幫你接滿了,那么這時你的水還是滿的,相對于水來說,他還是滿的,但是相對于杯子來說,他已經(jīng)被用過了兩次,一次是喝水,一次是接水,這就是ABA問題
從Java1.5開始JDK的atomic包里提供了一個類AtomicStampedReference來解決ABA問題。這個類的compareAndSet方法作用是首先檢查當(dāng)前引用是否等于預(yù)期引用,并且當(dāng)前標(biāo)志是否等于預(yù)期標(biāo)志,如果全部相等,則以原子方式將該引用和該標(biāo)志的值設(shè)置為給定的更新值。
生活中:
你接了一杯水,然后旁邊放上一張登記表,這個時候你同事過來,直接喝掉了一半,然后登記上,XXX喝掉了一半的水,然后去給你接滿了,再次登記上,我給你接滿了,這時,ABA的問題就得到了解決,你一看這個表就知道了一切
開銷問題:
在自旋或者死循環(huán)中不斷進(jìn)行CAS操作,但是長期操作不成功,CPU不斷的循環(huán),帶來的開銷問題
自旋CAS如果長時間不成功,會給CPU帶來非常大的執(zhí)行開銷。如果JVM能支持處理器提供的pause指令那么效率會有一定的提升,pause指令有兩個作用,第一它可以延遲流水線執(zhí)行指令(de-pipeline),使CPU不會消耗過多的執(zhí)行資源,延遲的時間取決于具體實現(xiàn)的版本,在一些處理器上延遲時間是零。第二它可以避免在退出循環(huán)的時候因內(nèi)存順序沖突(memory order violation)而引起CPU流水線被清空(CPU pipeline flush),從而提高CPU的執(zhí)行效率。
只能保證一個共享變量的原子操作
當(dāng)對一個共享變量執(zhí)行操作時,我們可以使用循環(huán)CAS的方式來保證原子操作,但是對多個共享變量操作時,循環(huán)CAS就無法保證操作的原子性,這個時候就可以用鎖,或者有一個取巧的辦法,就是把多個共享變量合并成一個共享變量來操作。比如有兩個共享變量i=2,j=a,合并一下ij=2a,然后用CAS來操作ij。從Java1.5開始JDK提供了AtomicReference類來保證引用對象之間的原子性,你可以把多個變量放在一個對象里來進(jìn)行CAS操作。
CAS的目的:
利用CPU的CAS指令,同時借助JNI來完成Java的非阻塞算法。其它原子操作都是利用類似的特性完成的。而整個J.U.C都是建立在CAS之上的,因此對于synchronized阻塞算法,J.U.C在性能上有了很大的提升。
JDK中相關(guān)原子操作類的使用:
更新基本類型類:AtomicBoolean,AtomicInteger,AtomicLong
更新數(shù)組類:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArrat
更新引用類型:
AtomicReference,AtomicMarkableReference,AtomicStampedReference
原子更新字段類:
AtomicReferenceFieldUpdater,AtomicIntegerFieldUpdater,AtomicLongFieldUpdater
理論已經(jīng)理解的差不多了,接下來寫寫代碼
使用AtomicInteger
package?org.dance.day3;
import?java.util.concurrent.atomic.AtomicInteger;
/**
?*?使用原子類int類型
?*?@author?ZYGisComputer
?*/
public?class?UseAtomicInt?{
????static?AtomicInteger?atomicInteger?=?new?AtomicInteger(10);
????public?static?void?main(String[]?args)?{
????????//?10->11?10先去再增加
????????System.out.println(atomicInteger.getAndIncrement());
????????//?11->12?12先增加再取
????????System.out.println(atomicInteger.incrementAndGet());
????????//?獲取
????????System.out.println(atomicInteger.get());
????}
}
返回值:
10
12
12
通過返回值可以看到,第一個是先獲取返回值后累加1,第二個是先累加1后再返回,第三個是獲取當(dāng)前值
使用AtomicIntegerArray
package?org.dance.day3;
import?java.util.concurrent.atomic.AtomicIntegerArray;
/**
?*?使用原子類int[]
?*?@author?ZYGisComputer
?*/
public?class?UseAtomicIntegerArray?{
????static?int[]?values?=?new?int[]{1,2};
????static?AtomicIntegerArray?atomicIntegerArray?=?new?AtomicIntegerArray(values);
????public?static?void?main(String[]?args)?{
????????//改變的第一個參數(shù)是?數(shù)組的下標(biāo),第二個是新值
????????atomicIntegerArray.getAndSet(0,3);
????????//?獲取原子數(shù)組類中的下標(biāo)為0的值
????????System.out.println(atomicIntegerArray.get(0));
????????//?獲取源數(shù)組中下標(biāo)為0的值
????????System.out.println(values[0]);
????}
}
返回結(jié)果:
3
1通過返回結(jié)果我們可以看到,源數(shù)組中的值并沒有改變,只有引用中的值發(fā)生了改變,這是則么回事?
/**
?????*?Creates?a?new?AtomicIntegerArray?with?the?same?length?as,?and
?????*?all?elements?copied?from,?the?given?array.
?????*
?????*?@param?array?the?array?to?copy?elements?from
?????*?@throws?NullPointerException?if?array?is?null
?????*/
????public?AtomicIntegerArray(int[]?array)?{
????????//?Visibility?guaranteed?by?final?field?guarantees
????????this.array?=?array.clone();
????}
通過看源碼我們得知他是調(diào)用了數(shù)組的克隆方法,克隆了一個一模一樣的
使用AtomicReference
package?org.dance.day3;
import?java.util.concurrent.atomic.AtomicReference;
/**
?*?使用原子類引用類型
?*?@author?ZYGisComputer
?*/
public?class?UseAtomicReference?{
????static?AtomicReference?atomicReference?=?new?AtomicReference<>();
????public?static?void?main(String[]?args)?{
????????UserInfo?src?=?new?UserInfo("彼岸舞",18);
????????//?使用原子引用類包裝一下
????????atomicReference.set(src);
????????UserInfo?target?=?new?UserInfo("彼岸花",19);
????????//?這里就是CAS改變了,這個應(yīng)用類就好像一個容器也就是內(nèi)存V,而src就是原值A(chǔ),target就是新值B
????????//?期望原值是src,如果是的話,改變?yōu)閠arget,否則不變
????????atomicReference.compareAndSet(src,target);
????????System.out.println(atomicReference.get());
????????System.out.println(src);
????}
????static?class?UserInfo{
????????private?String?name;
????????private?int?age;
????????@Override
????????public?String?toString()?{
????????????return?"UserInfo{"?+
????????????????????"name='"?+?name?+?'\''?+
????????????????????",?age="?+?age?+
????????????????????'}';
????????}
????????public?UserInfo()?{
????????}
????????public?UserInfo(String?name,?int?age)?{
????????????this.name?=?name;
????????????this.age?=?age;
????????}
????????public?String?getName()?{
????????????return?name;
????????}
????????public?void?setName(String?name)?{
????????????this.name?=?name;
????????}
????????public?int?getAge()?{
????????????return?age;
????????}
????????public?void?setAge(int?age)?{
????????????this.age?=?age;
????????}
????}
}
返回結(jié)果:
UserInfo{name='彼岸花',?age=19}
UserInfo{name='彼岸舞',?age=18}通過返回結(jié)果可以直觀的看到,原子引用類中的值發(fā)生了改變,但是源對象src卻沒有改變,因為原子引用類和原對象本身是兩個東西,CAS后就可以理解為內(nèi)存中的東西變了,也可以說是引用變了,他只能保證你在改變這個引用的時候保證是原子性的
記得之前上面說的ABA問題吧,在這里就是解決代碼
JDK中提供了兩種解決ABA問題的類
AtomicStampedReference
AtomicStampedReference,里面是用int類型,他關(guān)心的是被人動過幾次
AtomicMarkableReference
AtomicMarkableReference,里面是用boolean類型,他只關(guān)心這個版本有沒有人動過
?兩個類關(guān)心的點不一樣,側(cè)重的方向不一樣,就像之前說的喝水問題,AtomicStampedReference關(guān)心的是,被幾個人動過,而AtomicMarkableReference關(guān)心的是有沒有人動過
使用AtomicStampedReference解決ABA問題
package?org.dance.day3;
import?java.util.concurrent.atomic.AtomicStampedReference;
/**
?*?使用版本號解決ABA問題
?*?@author?ZYGisComputer
?*/
public?class?UseAtomicStampedReference?{
????/**
?????*?構(gòu)造參數(shù)地第一個是默認(rèn)值,第二個就是版本號
?????*/
????static?AtomicStampedReference?atomicStampedReference?=?new?AtomicStampedReference<>("src",0);
????public?static?void?main(String[]?args)?throws?InterruptedException?{
????????//?獲取初始版本號
????????final?int?oldStamp?=?atomicStampedReference.getStamp();
????????//?獲取初始值
????????final?String?oldValue?=?atomicStampedReference.getReference();
????????System.out.println("oldValue:"+oldValue+"?oldStamp:"+oldStamp);
????????Thread?success?=?new?Thread(new?Runnable()?{
????????????@Override
????????????public?void?run()?{
????????????????System.out.println(Thread.currentThread().getName()+",當(dāng)前變量值:"+oldValue+"當(dāng)前版本號:"+oldStamp);
????????????????//?變更值和版本號
????????????????/**
?????????????????*?第一個參數(shù):期望值
?????????????????*?第二個參數(shù):新值
?????????????????*?第三個參數(shù):期望版本號
?????????????????*?第四個參數(shù):新版本號
?????????????????*/
????????????????boolean?b?=?atomicStampedReference.compareAndSet(oldValue,?oldValue?+?"java",?oldStamp,?oldStamp?+?1);
????????????????System.out.println(b);
????????????}
????????});
????????Thread?error?=?new?Thread(new?Runnable()?{
????????????@Override
????????????public?void?run()?{
????????????????//?獲取原值
????????????????String?sz?=?atomicStampedReference.getReference();
????????????????int?stamp?=?atomicStampedReference.getStamp();
????????????????System.out.println(Thread.currentThread().getName()+",當(dāng)前變量值:"+sz+"當(dāng)前版本號:"+stamp);
????????????????boolean?b?=?atomicStampedReference.compareAndSet(oldValue,?oldValue?+?"C",?oldStamp,?oldStamp?+?1);
????????????????System.out.println(b);
????????????}
????????});
????????success.start();
????????success.join();
????????error.start();
????????error.join();
????????System.out.println(atomicStampedReference.getReference()+":"+atomicStampedReference.getStamp());
????}
}
返回結(jié)果:
oldValue:src?oldStamp:0
Thread-0,當(dāng)前變量值:src當(dāng)前版本號:0
true
Thread-1,當(dāng)前變量值:srcjava當(dāng)前版本號:1
false
srcjava:1
通過返回結(jié)果可以觀察到,原始值是src,版本是0,然后使用join方法使我們的正確線程確保咋錯誤線程之前執(zhí)行完畢,當(dāng)正確線程執(zhí)行完畢后,會把值改為srcjava,版本改為+1,然后執(zhí)行錯誤的線程,錯誤的線程在嘗試去改值的時候,發(fā)現(xiàn)期望的值是src,但是值已經(jīng)被改變成srcjava了,并且期望的版本是0,但是版本已經(jīng)被改為1了,所以他無法修改,在兩個線程都執(zhí)行完畢之后,打印的值是?srcjava,版本是1,成功的解決了ABA問題,當(dāng)然在這里面我的期望值是還是src,也可以改為src+java但是因為版本不一樣也是無法修改成功的;親測沒問題
原子更新字段類就不寫了,那個使用比較麻煩,如果多個字段的話,就直接使用AtomicReference類就可以了
粉絲福利:108本java從入門到大神精選電子書領(lǐng)取
???
?長按上方鋒哥微信二維碼?2 秒 備注「1234」即可獲取資料以及 可以進(jìn)入java1234官方微信群
感謝點贊支持下哈?
