理會CAS和CAS:java
有時候面試官面試問你的時候,會問,談談你對CAS的理解,這時應該有不少人,就會比較懵,固然,我也會比較懵,固然我和不少人的懵不一樣,不少人可能,並不知道CAS是一個什麼東西,而在我看來我是不知道他問的是那個CAS面試
我通常會問面試官,問他問的CAS是"原子操做",仍是"單點登陸"算法
由於在JAVA併發中的原子操做是稱爲CAS的,也就是英文單詞CompareAndSwap的縮寫,中文意思是:比較並替換。編程
可是在企業應用中CAS也被稱爲企業級開源單點登陸解決方案,是 Central Authentication Service 的縮寫 —— 中央認證服務,一種獨立開放指令協議,是 Yale 大學發起的一個企業級開源項目,旨在爲 Web 應用系統提供一種可靠的 SSO 解決方案。數組
CAS(Compare And Swap):安全
咱們先要學習的是併發編程中的CAS,也就是原子操做網絡
那麼,什麼是原子操做?如何實現原子操做?併發
什麼是原子操做:ide
原子,也是最小單位,是一個不可再分割的單位,不可被中斷的一個或者一系列操做性能
CAS是以一種無鎖的方式實現併發控制,在實際狀況下,同時操做一個對象的機率很是小,因此多數加鎖操做作的基本是無用功
CAS以一種樂觀鎖的方式實現併發控制
如何實現原子操做:
Java能夠經過鎖和循環CAS的方式實現原子操做
爲何要有CAS:
CAS就是比較而且替換的一個原子操做,在CPU的指令級別上進行保證
爲何要有CAS:
Sync是基於阻塞的鎖的機制,
1:被阻塞的線程優先級很高
2:拿到鎖的線程一直不釋放鎖則麼辦
3:大量的競爭,消耗CPU,同時帶來死鎖或者其餘線程安全
由於經過鎖實現原子操做時,其餘線程必須等待已經得到鎖的線程運行完車以後才能獲取鎖,這樣就會佔用系統大量資源
CAS原理:
從CPU指令級別保證這是一個原子操做
CAS包含哪些參數:
三個運算符:
一個內存地址V
一個指望的值A
一個新值B
基本思路:
若是地址V上的值和指望的值A相等,就給地址V賦值新值B,若是不是,不作任何操做
循環CAS:
在一個(死)循環中[for(;;)]裏不斷進行CAS操做,直到成功爲止(自旋操做即死循環)
CAS問題:
ABA問題:
那麼什麼是ABA問題?就是內存中本來是A,而後經過CAS變成了B,而後再次經過CAS變成了A,這個過程當中,相對於結果來講,是沒有任何改變的,可是相對於內存來講,至少發生過兩次變化,這就是ABA問題
生活中:
就像你接了一杯水,這時水是滿的,可是這個時候,你的同時很渴,過來拿你的水直接喝掉了一半,這時水剩下了一半,接着,你的同事又從新把你的水幫你接滿了,那麼這時你的水仍是滿的,相對於水來講,他仍是滿的,可是相對於杯子來講,他已經被用過了兩次,一次是喝水,一次是接水,這就是ABA問題
從Java1.5開始JDK的atomic包裏提供了一個類AtomicStampedReference來解決ABA問題。這個類的compareAndSet方法做用是首先檢查當前引用是否等於預期引用,而且當前標誌是否等於預期標誌,若是所有相等,則以原子方式將該引用和該標誌的值設置爲給定的更新值。
生活中:
你接了一杯水,而後旁邊放上一張登記表,這個時候你同事過來,直接喝掉了一半,而後登記上,XXX喝掉了一半的水,而後去給你接滿了,再次登記上,我給你接滿了,這時,ABA的問題就獲得瞭解決,你一看這個表就知道了一切
開銷問題:
在自旋或者死循環中不斷進行CAS操做,可是長期操做不成功,CPU不斷的循環,帶來的開銷問題
自旋CAS若是長時間不成功,會給CPU帶來很是大的執行開銷。若是JVM能支持處理器提供的pause指令那麼效率會有必定的提高,pause指令有兩個做用,第一它能夠延遲流水線執行指令(de-pipeline),使CPU不會消耗過多的執行資源,延遲的時間取決於具體實現的版本,在一些處理器上延遲時間是零。第二它能夠避免在退出循環的時候因內存順序衝突(memory order violation)而引發CPU流水線被清空(CPU pipeline flush),從而提升CPU的執行效率。
只能保證一個共享變量的原子操做
當對一個共享變量執行操做時,咱們可使用循環CAS的方式來保證原子操做,可是對多個共享變量操做時,循環CAS就沒法保證操做的原子性,這個時候就能夠用鎖,或者有一個取巧的辦法,就是把多個共享變量合併成一個共享變量來操做。好比有兩個共享變量i=2,j=a,合併一下ij=2a,而後用CAS來操做ij。從Java1.5開始JDK提供了AtomicReference類來保證引用對象之間的原子性,你能夠把多個變量放在一個對象裏來進行CAS操做。
CAS的目的:
利用CPU的CAS指令,同時藉助JNI來完成Java的非阻塞算法。其它原子操做都是利用相似的特性完成的。而整個J.U.C都是創建在CAS之上的,所以對於synchronized阻塞算法,J.U.C在性能上有了很大的提高。
JDK中相關原子操做類的使用:
更新基本類型類:AtomicBoolean,AtomicInteger,AtomicLong
更新數組類:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArrat
更新引用類型:AtomicReference,AtomicMarkableReference,AtomicStampedReference
原子更新字段類:AtomicReferenceFieldUpdater,AtomicIntegerFieldUpdater,AtomicLongFieldUpdater
理論已經理解的差很少了,接下來寫寫代碼
使用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後再返回,第三個是獲取當前值
使用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) { //改變的第一個參數是 數組的下標,第二個是新值 atomicIntegerArray.getAndSet(0,3); // 獲取原子數組類中的下標爲0的值 System.out.println(atomicIntegerArray.get(0)); // 獲取源數組中下標爲0的值 System.out.println(values[0]); } }
返回結果:
3 1
經過返回結果咱們能夠看到,源數組中的值並無改變,只有引用中的值發生了改變,這是則麼回事?
/** * 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(); }
經過看源碼咱們得知他是調用了數組的克隆方法,克隆了一個如出一轍的
使用AtomicReference
package org.dance.day3; import java.util.concurrent.atomic.AtomicReference; /** * 使用原子類引用類型 * @author ZYGisComputer */ public class UseAtomicReference { static AtomicReference<UserInfo> atomicReference = new AtomicReference<>(); public static void main(String[] args) { UserInfo src = new UserInfo("彼岸舞",18); // 使用原子引用類包裝一下 atomicReference.set(src); UserInfo target = new UserInfo("彼岸花",19); // 這裏就是CAS改變了,這個應用類就好像一個容器也就是內存V,而src就是原值A,target就是新值B // 指望原值是src,若是是的話,改變爲target,不然不變 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; } } }
返回結果:
UserInfo{name='彼岸花', age=19}
UserInfo{name='彼岸舞', age=18}
經過返回結果能夠直觀的看到,原子引用類中的值發生了改變,可是源對象src卻沒有改變,由於原子引用類和原對象自己是兩個東西,CAS後就能夠理解爲內存中的東西變了,也能夠說是引用變了,他只能保證你在改變這個引用的時候保證是原子性的
記得以前上面說的ABA問題吧,在這裏就是解決代碼
JDK中提供了兩種解決ABA問題的類
AtomicStampedReference
AtomicStampedReference,裏面是用int類型,他關心的是被人動過幾回
AtomicMarkableReference
AtomicMarkableReference,裏面是用boolean類型,他只關心這個版本有沒有人動過
兩個類關心的點不同,側重的方向不同,就像以前說的喝水問題,AtomicStampedReference關心的是,被幾我的動過,而AtomicMarkableReference關心的是有沒有人動過
使用AtomicStampedReference解決ABA問題
package org.dance.day3; import java.util.concurrent.atomic.AtomicStampedReference; /** * 使用版本號解決ABA問題 * @author ZYGisComputer */ public class UseAtomicStampedReference { /** * 構造參數地第一個是默認值,第二個就是版本號 */ static AtomicStampedReference<String> 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()+",當前變量值:"+oldValue+"當前版本號:"+oldStamp); // 變動值和版本號 /** * 第一個參數:指望值 * 第二個參數:新值 * 第三個參數:指望版本號 * 第四個參數:新版本號 */ 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()+",當前變量值:"+sz+"當前版本號:"+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()); } }
返回結果:
oldValue:src oldStamp:0 Thread-0,當前變量值:src當前版本號:0 true Thread-1,當前變量值:srcjava當前版本號:1 false srcjava:1
經過返回結果能夠觀察到,原始值是src,版本是0,而後使用join方法使咱們的正確線程確保咋錯誤線程以前執行完畢,當正確線程執行完畢後,會把值改成srcjava,版本改成+1,而後執行錯誤的線程,錯誤的線程在嘗試去改值的時候,發現指望的值是src,可是值已經被改變成srcjava了,而且指望的版本是0,可是版本已經被改成1了,因此他沒法修改,在兩個線程都執行完畢以後,打印的值是 srcjava,版本是1,成功的解決了ABA問題,固然在這裏面個人指望值是仍是src,也能夠改成src+java可是由於版本不同也是沒法修改爲功的;親測沒問題
原子更新字段類就不寫了,那個使用比較麻煩,若是多個字段的話,就直接使用AtomicReference類就能夠了
做者:彼岸舞
時間:2020\10\04
內容關於:併發編程
本文來源於網絡,只作技術分享,一律不負任何責任