相信我們都知道樂觀鎖的底層是利用了CAS機制實現(如有不懂,請看上篇文章)你真的瞭解樂觀鎖、悲觀鎖嗎?
Java的CAS底層實現
我們先來看看cnt.incrementAndGet();這個自增方法的原始碼
public final intincrementAndGet() {
for (;;) {//失敗,迴圈重試
int current = get();//讀取值
int next = current + 1;//修改值
if (compareAndSet(current, next))//比較並且賦值
return next;
}
}
private volatileint value;
public final int get(){
return value;
}
複製程式碼
這裡需要注意一下這個get方法,為何要在宣告value時候使用volatile關鍵字呢? 那是因為volatile關鍵字保證變數的可見性!保證獲取當前值是記憶體中的最新值
而這段程式碼也是是ABA產生的原因
在以上程式碼中,可以看到compareAndSet(int expect, intupdate)的第1個引數,傳進去的並不是版本號,而是資料的舊值。也就是說,它認為,只要資料的舊值expect = 資料當前的值,則說明在此期間沒有其他執行緒修改過此資料,則把資料修改為新值update。
這種比較值,而不是比較版本號的做法,會產生經典的ABA問題。而這,也正是AtomicStampedReference要解決的。
public final boolean compareAndSet(int expect,int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
//value成員變數在記憶體中的偏移量
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
複製程式碼
接下來我們看看compareAndSet 方法,很明顯我們需要知道什麼是unsafe?compareAndSwapInt方法的具體含義,以及這幾個引數所表示的意義。
到底什麼是unsafe?java不像c/c++可以直接訪問底層作業系統,但是JVM卻留了一手,unsafe可以為我們提供硬體級別的操作。
compareAndSwapInt方法保證了Compare和Swap操作之間的原子性操作。
CAS機制中使用了3個運算元:需要讀寫的記憶體值 V,進行比較的值 A,擬寫入的新值 B,而unsafe的compareAndSwapInt方法引數包括三個元素,valueOffset引數代表了V,expect引數代表了A,update引數代表了B
ABA問題
線上程1改資料期間,執行緒2把資料改為A,再改為B,再改回到A。這個時候,執行緒1做CAS的時候,如果只是比較值,則它會認為資料在此期間沒有被改動過,而實際上資料已被執行緒2改動過3次。
我們舉一個例子
假設銀行有一個遵循CAS原理的提款機,小明有100元存款,現在需要取出50元。
由於取款機出現了點小問題,取款操作被提交了兩次,開啟了兩個執行緒,兩個執行緒都是獲取當前餘額100元,更新成50元。
理論上一個執行緒成功了,另一個執行緒失敗了,餘額只被扣了一次,餘額為50.
但是這時候,執行緒1執行成功了,執行緒2因為部分原因阻塞,這時候小明媽媽往小明賬戶中匯款50元,這時候賬戶餘額為100元,此時執行緒2恢復執行,因為阻塞之前獲取的賬戶金額為100元,此時賬戶金額也為100元,執行緒2認為一致執行成功,餘額會被更新為50元。而正確餘額應該是100元。
這就是1A-2B-3A問題
那該怎麼解決這個問題呢?其實邏輯也很簡單,我們只需要加個版本號就行了,在Compare期間不僅要比較A和V中的值,還需要比較版本號是否一致。面我們看看加入版本號的實現
//舊值,新值,舊版本號,新版本號
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp)
{
ReferenceIntegerPair<V> current = atomicRef.get();
return expectedReference ==current.reference &&
expectedStamp == current.integer &&
((newReference == current.reference &&
newStamp == current.integer) ||
atomicRef.compareAndSet(current,
newReferenceIntegerPair<V>(newReference,
newStamp)));
}
private static class ReferenceIntegerPair<T> {
private final T reference; //值
private final int integer; //版本號
ReferenceIntegerPair(T r, int i) {
reference = r; integer = i;
}
}
複製程式碼
上面的atomicRef.compareAndSet(…)的第一個引數,傳入的是一個ReferenceIntegerPair物件,它裡面包含了2個欄位:值 + 版本號。這也就意味著,它同時比較了值和版本號。
– 值不等,則肯定被其他執行緒改過了,不用再比較版本號,cas提交失敗;
值相等,再比較版本號,如果版本號也相等,則說明真的沒有被改過,cas提交成功;
值相等,版本號不等,則就是出現了ABA,CAS提交失敗。
----------------END----------------
喜歡本文的朋友,歡迎關注我的公眾號程式設計師阿寶,檢視更多精彩內容