前言:關於同步,不少人都知道synchronized,Reentrantlock等加鎖技術,這種方式也很好理解,是在線程訪問的臨界區資源上創建一個阻塞機制,須要線程等待java
其它線程釋放了鎖,它才能運行。這種方式很顯然是奏效的,可是它卻帶來一個很大的問題:程序的運行效率。線程的上下文切換是很是耗費資源的,而等待又會有必定的時間消耗,那麼有沒有一種方式既能控制程序的同步效果,又能避免這種鎖帶來的消耗呢?答案就是無鎖技術,本篇博客討論的中心就是無鎖。程序員
一:有鎖與無鎖數據庫
二:cas技術原理api
三:AtomicInteger與unsafe類安全
四:經典的ABA問題與解決方法app
五:總結ide
正文性能
一:有鎖與無鎖測試
1.1:悲觀鎖與樂觀鎖this
數據庫有兩種鎖,悲觀鎖的原理是每次實現數據庫的增刪改的時候都進行阻塞,防止數據發生髒讀;樂觀鎖的原理是在數據庫更新的時候,用一個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,而不要嘗試本身去作,無鎖是一個雙刃劍,用好了,絕對可讓性能比鎖有很大的提高,用很差就很容易形成數據污染與髒讀,望謹慎之。