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中取資料
3)解決方式一:通過synchronized關鍵字
使用synchronized可以保證可見行和原子性
Java記憶體模型中定義了lock和unlock兩種操作:
lock(鎖定):作用與主記憶體的變數,它把一個變數標識為一條執行緒獨佔的狀態(可簡單理解為:變數此時只能被一條執行緒使用)。
unlock(解鎖):作用域主記憶體的變數,它把一個處於鎖定狀態的變數釋放出來,釋放出來後的變數才可以被其他執行緒鎖定。
原子行的保證:lock和unlock間的變數是被執行緒獨佔的
可見性的保證:對一個變數執行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