前言:關於同步,很多人都知道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,而不要嘗試自己去做,無鎖是一個雙刃劍,用好了,絕對可以讓性能比鎖有很大的提升,用不好就很容易造成數據污染與臟讀,望謹慎之。