CAS無鎖技術
前言:關於同步,很多人都知道synchronized,Reentrantlock等加鎖技術,這種方式也很好理解,是線上程訪問的臨界區資源上建立一個阻塞機制,需要執行緒等待
其它執行緒釋放了鎖,它才能執行。這種方式很顯然是奏效的,但是它卻帶來一個很大的問題:程式的執行效率。執行緒的上下文切換是非常耗費資源的,而等待又會有一定的時間消耗,那麼有沒有一種方式既能控制程式的同步效果,又能避免這種鎖帶來的消耗呢?答案就是無鎖技術,本篇部落格討論的中心就是無鎖。
一:有鎖與無鎖
二:cas技術原理
三:AtomicInteger與unsafe類
四:經典的ABA問題與解決方法
五:總結
正文
一:有鎖與無鎖
1.1:悲觀鎖與樂觀鎖
資料庫有兩種鎖,悲觀鎖的原理是每次實現資料庫的增刪改的時候都進行阻塞,防止資料發生髒讀;樂觀鎖的原理是在資料庫更新的時候,用一個version欄位來記錄版本號,然後通過比較是不是自己要修改的版本號再進行修改。這其中就引出了一種比較替換的思路來實現資料的一致性,事實上,cas也是基於這樣的原理。
二:CAS技術原理
2.1:cas是什麼?
cas的英文翻譯全稱是compare and set ,也就是比較替換技術,·它包含三個引數,CAS(V,E,N),其中V(variile)表示欲更新的變數,E(Excepted)表示預期的值,N(New)表示新值,只有當V等於E值的時候嗎,才會將V的值設為N,如果V值和E值不同,則說明已經有其它執行緒對該值做了更新,則當前執行緒什麼都不做,直接返回V值。
舉個例子,假如現在有一個變數int a=5;我想要把它更新為6,用cas的話,我有三個引數cas(5,5,6),我們要更新的值是5,找到了a=5,符合V值,預期的值也是5符合,然後就會把N=6更新給a,a的值就會變成6;
2.2:cas的優點
2.2.1cas是以樂觀的態度執行的,它總是認為當前的執行緒可以完成操作,當多個執行緒同時使用CAS的時候只有一個最終會成功,而其他的都會失敗。這種是由欲更新的值做的一個篩選機制,只有符合規則的執行緒才能順利執行,而其他執行緒,均會失敗,但是失敗的執行緒並不會被掛起,僅僅是嘗試失敗,並且允許再次嘗試(當然也可以主動放棄)
2.2.2:cas可以發現其他執行緒的干擾,排除其他執行緒造成的資料汙染
三:AtomicInteger與unsafe類
CAS在jdk5.0以後就被得到廣泛的利用,而AtomicInteger是很典型的一個類,接下來我們就來著重研究一下這個類:
3.1:AtomicInteger
關於Integer,它是final的不可變類,AtomicInteget可以把它視為一種整數類,它並非是fianl的,但卻是執行緒安全的,而它的實現就是著名的CAS了,下面是一些它的常用方法:
public final int getAndSet(int newValue); public final boolean compareAndSet(int expect, int update); public final boolean weakCompareAndSet(int expect, int update); public final int getAndIncrement(); public final int getAndDecrement(); public final int addAndGet(int delta); public final int decrementAndGet(); public final int incrementAndGet()
其中主要的方法就是compareAndSet,我們來測試一下這個方法,首先先給定一個值是5,我們現在要把它改成2,如果expect傳的是1,程式會輸出什麼呢?
public class TestAtomicInteger { public static void main(String[] args) { AtomicInteger atomicInteger = new AtomicInteger(5); boolean isChange = atomicInteger.compareAndSet(1, 2); int i = atomicInteger.get(); System.out.println("是否變化:"+isChange); System.out.println(i); } }
//outPut:
是否變化:false 5
boolean isChange = atomicInteger.compareAndSet(5, 2);
如果我們把期望值改成5的話,最後的輸出結果將是: // 是否變化:true 2
結論:只有當期望值與要改的值一致的時候,cas才會替換原始的值,設定成新值
3.2:測試AtomicInteger的執行緒安全性
為此我新建了10個執行緒,每個執行緒對它的值自增5000次,如果是執行緒安全的,應該輸出:50000
public class TestAtomicInteger { static AtomicInteger number=new AtomicInteger(0); public static class AddThread implements Runnable{ @Override public void run() { for (int i = 0; i < 5000; i++) { number.incrementAndGet(); } } } public static void main(String[] args) throws InterruptedException { Thread[] threads=new Thread[10]; for (int i = 0; i < threads.length; i++) { threads[i]=new Thread(new AddThread()); } for (int i = 0; i < threads.length; i++) { threads[i].start(); } for (int i = 0; i < threads.length; i++) { threads[i].join(); } System.out.println(number); } }
最後重複執行了很多次都是輸出:50000
3.3:unsafe類
翻以下這個方法的原始碼,可以看到其中是這樣實現的:
public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); }
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
主要交給了unsafe類的compareAndSwapInt的方法,再翻以下可以看到是native的,也就是本地呼叫C++實現的原始碼,這裡我們就不深究了。關於unsafe類,它有一個最重要的點就是jdk的開發人員認為這個類是很危險的,所以是unsafe!因此不建議程式設計師呼叫這個類,為此他們還對這個類做了一個絕妙的處理,讓你無法使用它:
public static Unsafe getUnsafe() { Class class= Reflection.getCallerClass(); if (!VM.isSystemDomainLoader(class.getClassLoader())) { throw new SecurityException("Unsafe"); } else { return theUnsafe; } }
public static boolean isSystemDomainLoader(ClassLoader var0) {
return var0 == null;
}
//outPut
Exception in thread "main" java.lang.SecurityException: Unsafe at sun.misc.Unsafe.getUnsafe(Unsafe.java:90)
這個方法實現的原理主要是類的載入機制,應用類的類載入器是有applicationClassLoder載入的,而jdk的類,比如關鍵庫,rt.jar是由Bootstrap載入的,而BootStrapclassLoader是最上層載入庫,它其實是沒有java物件的,因為jdk的常用類比如(AtomicInteger)載入的時候它會返回null,而我們自定義的類一定不會返回null,就會丟擲異常!
3.4:compareAndSet的方法原理
public final int incrementAndGet(){ for(;;){ int current=get(); int next=current+1; if(compareAndSet(current,next)){
return next; } } }
可以看出這是在一個無限的for迴圈裡,然後獲取當前的值,再給他加1(固定寫死的值,每次自增1)。然後通過comePareandSet把當前的值和通過+1獲取的值經過cas設值,這個方法返回一個boolean值,當成功的時候就返回當前的值,這樣就保證了只有一個執行緒可以設值成功。注意:這裡是一個死迴圈,只有當前值等於設定後的+1的值時,它才會跳出迴圈。這也證明cas是一個不斷嘗試的過程
四:經典的ABA問題與解決方法
4.2:AbA問題的產生
要了解什麼是ABA問題,首先我們來通俗的看一下這個例子,一家火鍋店為了生意推出了一個特別活動,凡是在五一期間的老使用者凡是卡里餘額小於20的,贈送10元,但是這種活動沒人只可享受一次。然後火鍋店的後臺程式設計師小王開始工作了,很簡單就用cas技術,先去使用者卡里的餘額,然後包裝成AtomicInteger,寫一個判斷,開啟10個執行緒,然後判斷小於20的,一律加20,然後就很開心的交差了。可是過了一段時間,發現賬面虧損的厲害,老闆起先的預支是2000塊,因為店裡的會員總共也就100多個,就算每人都符合條件,最多也就2000啊,怎麼預支了這麼多。小王一下就懵逼了,趕緊debug,tail -f一下日誌,這不看不知道,一看嚇一跳,有個客戶被充值了10次!
闡述:
假設有個執行緒A去判斷賬戶裡的錢此時是15,滿足條件,直接+20,這時候卡里餘額是35.但是此時不巧,正好在連鎖店裡,這個客人正在消費,又消費了20,此時卡里餘額又為15,執行緒B去執行掃描賬戶的時候,發現它又小於20,又用過cas給它加了20,這樣的話就相當於加了兩次,這樣迴圈往復肯定把老闆的錢就坑沒了!
本質:
ABA問題的根本在於cas在修改變數的時候,無法記錄變數的狀態,比如修改的次數,否修改過這個變數。這樣就很容易在一個執行緒將A修改成B時,另一個執行緒又會把B修改成A,造成casd多次執行的問題。
4.3:AtomicStampReference
AtomicStampReference在cas的基礎上增加了一個標記stamp,使用這個標記可以用來覺察資料是否發生變化,給資料帶上了一種實效性的檢驗。它有以下幾個引數:
//引數代表的含義分別是 期望值,寫入的新值,期望標記,新標記值 public boolean compareAndSet(V expected,V newReference,int expectedStamp,int newStamp); public V getRerference(); public int getStamp(); public void set(V newReference,int newStamp);
4.4:AtomicStampReference的使用例項
我們定義了一個money值為19,然後使用了stamp這個標記,這樣每次當cas執行成功的時候都會給原來的標記值+1。而後來的執行緒來執行的時候就因為stamp不符合條件而使cas無法成功,這就保證了每次
只會被執行一次。
public class AtomicStampReferenceDemo { static AtomicStampedReference<Integer> money =new AtomicStampedReference<Integer>(19,0); public static void main(String[] args) { for (int i = 0; i < 3; i++) { int stamp = money.getStamp(); System.out.println("stamp的值是"+stamp); new Thread(){ //充值執行緒 @Override public void run() { while (true){ Integer account = money.getReference(); if (account<20){ if (money.compareAndSet(account,account+20,stamp,stamp+1)){ System.out.println("餘額小於20元,充值成功,目前餘額:"+money.getReference()+"元"); break; } }else { System.out.println("餘額大於20元,無需充值"); } } } }.start(); } new Thread(){ @Override public void run() { //消費執行緒 for (int j = 0; j < 100; j++) { while (true){ int timeStamp = money.getStamp();//1 int currentMoney =money.getReference();//39 if (currentMoney>10){ System.out.println("當前賬戶餘額大於10元"); if (money.compareAndSet(currentMoney,currentMoney-10,timeStamp,timeStamp+1)){ System.out.println("消費者成功消費10元,餘額"+money.getReference()); break; } }else { System.out.println("沒有足夠的金額"); break; } try { Thread.sleep(1000); }catch (Exception ex){ ex.printStackTrace(); break; } } } } }.start(); } }
這樣實現了執行緒去充值和消費,通過stamp這個標記屬性來記錄cas每次設定值的操作,而下一次再cas操作時,由於期望的stamp與現有的stamp不一樣,因此就會設值失敗,從而杜絕了ABA問題的復現。
五:總結
本篇博文主要分享了cas的技術實現原理,對於無鎖技術,它有很多好處。同時,指出了它的弊端ABA問題,與此同時,也給出瞭解決方法。jdk原始碼中很多用到了cas技術,而我們自己如果使用無鎖技術,一定要謹慎處理ABA問題,最好使用jdk現有的api,而不要嘗試自己去做,無鎖是一個雙刃劍,用好了,絕對可以讓效能比鎖有很大的提升,用不好就很容易造成資料汙染與髒讀,望謹慎之。