1. 程式人生 > >CAS無鎖技術

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,而不要嘗試自己去做,無鎖是一個雙刃劍,用好了,絕對可以讓效能比鎖有很大的提升,用不好就很容易造成資料汙染與髒讀,望謹慎之。