CAS無鎖技術

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

相關文章
相關標籤/搜索