在談談java中的volatile一文中,我們提到過併發包中的原子類可以解決類似num++這樣的複合類操作的原子性問題,相比鎖機制,使用原子類更精巧輕量,效能開銷更小,本章就一起來分析下原子類的實現機理。
悲觀的解決方案(阻塞同步)
我們知道,num++看似簡單的一個操作,實際上是由1.讀取 2.加一 3.寫入 三步組成的,這是個複合類的操作(所以我們之前提到過的volatile是無法解決num++的原子性問題的),在併發環境下,如果不做任何同步處理,就會有執行緒安全問題。最直接的處理方式就是加鎖。
synchronized(this){
num++;
}複製程式碼
使用獨佔鎖機制來解決,是一種悲觀的併發策略,抱著一副“總有刁民想害朕”的態勢,每次運算元據的時候都認為別的執行緒會參與競爭修改,所以直接加鎖。同一刻只能有一個執行緒持有鎖,那其他執行緒就會阻塞。執行緒的掛起恢復會帶來很大的效能開銷,儘管jvm對於非競爭性的鎖的獲取和釋放做了很多優化,但是一旦有多個執行緒競爭鎖,頻繁的阻塞喚醒,還是會有很大的效能開銷的。所以,使用synchronized或其他重量級鎖來處理顯然不夠合理。
樂觀的解決方案(非阻塞同步)
樂觀的解決方案,顧名思義,就是很大度樂觀,每次運算元據的時候,都認為別的執行緒不會參與競爭修改,也不加鎖。如果操作成功了那最好;如果失敗了,比如中途確有別的執行緒進入並修改了資料(依賴於衝突檢測),也不會阻塞,可以採取一些補償機制,一般的策略就是反覆重試。很顯然,這種思想相比簡單粗暴利用鎖來保證同步要合理的多。
鑑於併發包中的原子類其實現機理都差不太多,本章我們就通過AtomicInteger這個原子類來進行分析。我們先來看看對於num++這樣的操作AtomicInteger是如何保證其原子性的。
/**
* Atomically increments by one the current value.
*
* @return the updated value
*/
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}複製程式碼
我們來分析下incrementAndGet的邏輯:
1.先獲取當前的value值
2.對value加一
3.第三步是關鍵步驟,呼叫compareAndSet方法來來進行原子更新操作,這個方法的語義是:
先檢查當前value是否等於current,如果相等,則意味著value沒被其他執行緒修改過,更新並返回true。如果不相等,compareAndSet則會返回false,然後迴圈繼續嘗試更新。
compareAndSet呼叫了Unsafe類的compareAndSwapInt方法
/**
* Atomically sets the value to the given updated value
* if the current value {@code ==} the expected value.
*
* @param expect the expected value
* @param update the new value
* @return true if successful. False return indicates that
* the actual value was not equal to the expected value.
*/
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}複製程式碼
Unsafe的compareAndSwapInt是個native方法,也就是平臺相關的。它是基於CPU的CAS指令來完成的。
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);複製程式碼
CAS(Compare-and-Swap)
CAS演算法是由硬體直接支援來保證原子性的,有三個運算元:記憶體位置V、舊的預期值A和新值B,當且僅當V符合預期值A時,CAS用新值B原子化地更新V的值,否則,它什麼都不做。
CAS的ABA問題
當然CAS也並不完美,它存在”ABA”問題,假若一個變數初次讀取是A,在compare階段依然是A,但其實可能在此過程中,它先被改為B,再被改回A,而CAS是無法意識到這個問題的。CAS只關注了比較前後的值是否改變,而無法清楚在此過程中變數的變更明細,這就是所謂的ABA漏洞。