1. 程式人生 > >Java中執行緒安全的加一(+1)操作的三種方式

Java中執行緒安全的加一(+1)操作的三種方式

1.鎖分為樂觀鎖和悲觀鎖,悲觀鎖總是假設每次的臨界區操作會產生衝突,如果多個執行緒同時需要訪問臨界區資源,就寧可犧牲效能讓執行緒進行等待。而樂觀鎖,它會假設對資源的訪問都是沒有衝突的,所有的執行緒都可以在不停頓的狀態下持續執行,如果遇到衝突,樂觀鎖採用的叫做比較交換(CAS Compare And Swap)來鑑別執行緒衝突,一旦檢測到衝突產生,就嘗試當前操作直到沒有衝突為止

2.鎖的必要性:

引例:變數i = 1,執行緒A進行了i+1操作,執行緒B也進行了i+1操作,經過兩次執行緒加法之後可能i等於2,並不一定是想象中的等於3。

1)直接進行併發操作

分析:下圖時Java的記憶體模型,假設主記憶體中有i=1,假設執行緒A先執行,執行緒A從主記憶體中讀取i = 1到本地記憶體A,並進行加一操作,線上程A將加一後的值從本地記憶體A寫回到主記憶體A前,執行緒B從主記憶體讀取了i的值到本地記憶體B,此時仍然為1,執行緒B對 本地記憶體B中的i = 1進行加一操作。然後執行緒A和執行緒B分別將本地記憶體中的2寫回到主記憶體中,所以最後結果是 i 的值都為2。

2)為了解決該問題,首先想到變數 i 使用volatile修飾

如果將i定義為volatile,此時保證了 i 的可見性,即當執行緒A修改了變數i的之後,新值對其他執行緒是立即可見的。但是仍然會有問題。問題出在i+1這條語句不是原子操作,i+1包含了3個操作:從工作記憶體讀取i的值,通過運算元棧進行加一,將值寫回到工作記憶體。再分析一下上面的過程,假設主記憶體中有i = 1,假設執行緒A先執行,執行緒A從主記憶體中讀取 i = 1到本地記憶體A,執行i+1操作的時候,先將i = 1取到運算元棧頂,此時執行緒二獲取了CPU的執行權,執行緒B從主記憶體中取出 i = 1,並進行加一操作,假設加一操作成功了,此時本地記憶體B中的i的值為2,同時本地記憶體A中i的值也被重新整理為2(可見性),然後執行緒A又獲取CPU的執行權,繼續進行i++的操作,但是注意剛剛的i=1被取到了運算元棧(運算元棧不會重新到本地記憶體A中取資料

),所以繼續加一後i的值為2,並把2寫回本地記憶體A,最終結果也為2。所以只滿足可見性還是不行,還需要滿足原子性

3)解決方式一:通過synchronized關鍵字

使用synchronized可以保證可見行和原子性

Java記憶體模型中定義了lock和unlock兩種操作:

lock(鎖定):作用與主記憶體的變數,它把一個變數標識為一條執行緒獨佔的狀態(可簡單理解為:變數此時只能被一條執行緒使用)。

unlock(解鎖):作用域主記憶體的變數,它把一個處於鎖定狀態的變數釋放出來,釋放出來後的變數才可以被其他執行緒鎖定。

原子行的保證:lock和unlock間的變數是被執行緒獨佔的

,Java中無法直接使用這兩條指令,但是可以用位元組碼指令monitorenter和monitorexit來隱式的使用這兩個操作,這兩個位元組碼指令反映到Java程式碼中就是同步塊——synchronized關鍵字,所以synchronized塊間的對變數的操作具有原子性

可見性的保證:對一個變數執行unlock操作之前,必須先把此變數同步回主記憶體中

3)解決方式二:使用CAS操作來解決

首先介紹CAS基本原理:它包含三個引數CAS(V,E,N)。V表示要跟新的變數,E表示預期值,N表示新值。僅當V值等於E值是,才會將V的值設定N,如果V值和E值不同,則說明已經有其他執行緒做了更新,則當前執行緒什麼都不做。

將i定義為AtomicInteger型別: 

 static AtomicInteger i = new AtomicInteger(1);

此時下面方法就能保證執行緒安全,A執行緒和B執行緒都執行加一操作後結果為3.

i.incrementAndGet();        //當前值加一

原理分析:

AtomicInteger中儲存了一個核心欄位:

private volatile int value;

incrementAndGet原始碼:

public final int incrementAndGet() {
    for (;;) {
        int current = get();                    //第三句                   
        int next = current + 1;               
        if (compareAndSet(current, next))       //第五句
            return next;
    }
}
public final int get() {
        return value;
}

incrementAndGet()保證了原子性,上面說了,int next = current + 1包含了多步操作,首先從從工作記憶體取出current,對其加一,然後賦值給next(至少包含這三步,其實轉成機器指令,有更多的操作),現在value的值為1,所以current = get() = 1,執行next = current + 1 = 2,此時對比期望值current的值(為1)和要更新的變數值(即value值),如果從第三句到第五句之間被別的執行緒改變了value的值,那麼期望值不等於要更新的變數值,操作失敗,繼續執行for(;;),如果操作成功就將value值替換成next的值(進行了加一操作)。

對於compareAndSet函式的實現。

public final boolean compareAndSet(int expect, int update) {   
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
public final native boolean compareAndSwapInt(Object o, long offset,
                                              int expected,
                                              int x);

compareAndSwapInt()方法是一個native方法,第一個引數o為給定的物件,offset為物件內的偏移量(其實就是一個欄位到物件頭部的偏移量,通過這個偏移量可以快速定位到value值),expected表示期望值,如果要更新的值value等於期望值,那麼就將x賦值給value。注意這裡的比較和賦值都是使用的CAS原子指令(通過呼叫CPU底層指令)完成的,因為要保證這兩步的原子行。

自己的理解:這裡對比的時候要更新值並不是取得工作記憶體的值,而是從主存中取值,所以使用偏移量直接定位到物件中的欄位地址處。

參考:

http://zl198751.iteye.com/blog/1848575

http://www.cnblogs.com/xrq730/p/4976007.html