
在Java并發中,我們最初接觸的應該就是synchronized關鍵字了,但是synchronized屬于重量級鎖,很多時候會引起性能問題,volatile也是個不錯的選擇,但是volatile不能保證原子性,只能在某些場合下使用,
像synchronized這種獨占鎖屬于悲觀鎖,它是在假設一定會發生沖突的,那么加鎖恰好有用,除此之外,還有樂觀鎖,樂觀鎖的含義就是假設沒有發生沖突,那么我正好可以進行某項操作,如果要是發生沖突呢,那我就重試直到成功,樂觀鎖最常見的就是CAS,
這里插一句,我整理了免費的Java架構學習資料,學習技術內容包含有:Spring,Dubbo,MyBatis, RPC, 原始碼分析,高并發、高性能、分布式,性能優化,微服務 高級架構開發等等,
需要的朋友可以點擊:這個!點這個,暗號:csdn,

我們在讀Concurrent包下的類的原始碼時,發現無論是ReenterLock內部的AQS,還是各種Atomic開頭的原子類,內部都應用到了CAS,最常見的就是我們在并發編程時遇到的i++這種情況,傳統的方法肯定是在方法上加上synchronized關鍵字:
public class Test {
public volatile int i;
public synchronized void add() {
i++;
}
}
但是這種方法在性能上可能會差一點,我們還可以使用AtomicInteger,就可以保證i原子的++了,
public class Test {
public AtomicInteger i;
public void add() {
i.getAndIncrement();
}
}
我們來看getAndIncrement的內部:
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
再深入到getAndAddInt():
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
這里我們見到compareAndSwapInt這個函式,它也是CAS縮寫的由來,那么仔細分析下這個函式做了什么呢?
首先我們發現compareAndSwapInt前面的this,那么它屬于哪個類呢,我們看上一步getAndAddInt,前面是unsafe,這里我們進入的Unsafe類,這里要對Unsafe類做個說明,結合AtomicInteger的定義來說:
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
...
在AtomicInteger資料定義的部分,我們可以看到,其實實際存盤的值是放在value中的,除此之外我們還獲取了unsafe實體,并且定義了valueOffset,
再看到static塊,懂類加載程序的都知道,static塊的加載發生于類加載的時候,是最先初始化的,這時候我們呼叫unsafe的objectFieldOffset從Atomic類檔案中獲取value的偏移量,那么valueOffset其實就是記錄value的偏移量的,
再回到上面一個函式getAndAddInt,我們看var5獲取的是什么,通過呼叫unsafe的getIntVolatile(var1, var2),這是個native方法,具體實作到JDK原始碼里去看了,其實就是獲取var1中,var2偏移量處的值,var1就是AtomicInteger,var2就是我們前面提到的valueOffset,這樣我們就從記憶體里獲取到現在valueOffset處的值了,
現在重點來了,compareAndSwapInt(var1, var2, var5, var5 + var4)其實換成compareAndSwapInt(obj, offset, expect, update)比較清楚,意思就是如果obj內的value和expect相等,就證明沒有其他執行緒改變過這個變數,那么就更新它為update,如果這一步的CAS沒有成功,那就采用自旋的方式繼續進行CAS操作,取出乍一看這也是兩個步驟了啊,其實在JNI里是借助于一個CPU指令完成的,所以還是原子操作,
CAS底層原理
CAS底層使用JNI呼叫C代碼實作的,如果你有Hotspot原始碼,那么在Unsafe.cpp里可以找到它的實作:
static JNINativeMethod methods_15[] = {
//省略一堆代碼...
{CC"compareAndSwapInt", CC"("OBJ"J""I""I"")Z", FN_PTR(Unsafe_CompareAndSwapInt)},
{CC"compareAndSwapLong", CC"("OBJ"J""J""J"")Z", FN_PTR(Unsafe_CompareAndSwapLong)},
//省略一堆代碼...
};
我們可以看到compareAndSwapInt實作是在Unsafe_CompareAndSwapInt里面,再深入到Unsafe_CompareAndSwapInt:
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj);
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
p是取出的物件,addr是p中offset處的地址,最后呼叫了Atomic::cmpxchg(x, addr, e), 其中引數x是即將更新的值,引數e是原記憶體的值,代碼中能看到cmpxchg有基于各個平臺的實作,這里我選擇Linux X86平臺下的原始碼分析:
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
int mp = os::is_MP();
__asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
: "=a" (exchange_value)
: "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
: "cc", "memory");
return exchange_value;
}
這是一段小匯編,__asm__說明是ASM匯編,__volatile__禁止編譯器優化
// Adding a lock prefix to an instruction on MP machine
#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "
os::is_MP判斷當前系統是否為多核系統,如果是就給總線加鎖,所以同一芯片上的其他處理器就暫時不能通過總線訪問記憶體,保證了該指令在多處理器環境下的原子性,
在正式解讀這段匯編前,我們來了解下嵌入匯編的基本格式:
asm ( assembler template
: output operands /* optional */
: input operands /* optional */
: list of clobbered registers /* optional */
);
- template就是cmpxchgl %1,(%3)表示匯編模板
- output operands表示輸出運算元,=a對應eax暫存器
- input operand 表示輸入引數,%1 就是exchange_value, %3是dest, %4就是mp,
r表示任意暫存器,a還是eax暫存器 - list of clobbered registers就是些額外引數,cc表示編譯器cmpxchgl的執行將影響到標志暫存器,memory告訴編譯器要重新從記憶體中讀取變數的最新值,這點實作了volatile的感覺,
那么運算式其實就是cmpxchgl exchange_value ,dest,我們會發現%2也就是compare_value沒有用上,這里就要分析cmpxchgl的語意了,cmpxchgl末尾l表示運算元長度為4,上面已經知道了,cmpxchgl會默認比較eax暫存器的值即compare_value和exchange_value的值,如果相等,就把dest的值賦值給exchange_value,否則,將exchange_value賦值給eax,
最終,JDK通過CPU的cmpxchgl指令的支持,實作AtomicInteger的CAS操作的原子性,
CAS 的問題
ABA問題
CAS需要在操作值的時候檢查下值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那么使用CAS進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了,
這就是CAS的ABA問題, 常見的解決思路是使用版本號,在變數前面追加上版本號,每次變數更新的時候把版本號加一,那么A-B-A 就會變成1A-2B-3A,
目前在JDK的atomic包里提供了一個類AtomicStampedReference來解決ABA問題,這個類的compareAndSet方法作用是首先檢查當前參考是否等于預期參考,并且當前標志是否等于預期標志,如果全部相等,則以原子方式將該參考和該標志的值設定為給定的更新值,
回圈時間長開銷大
上面我們說過如果CAS不成功,則會原地自旋,如果長時間自旋會給CPU帶來非常大的執行開銷,
最后
還有Java核心知識點+全套架構師學習資料和視頻+一線大廠面試寶典+面試簡歷模板可以領取+阿里美團網易騰訊小米愛奇藝快手嗶哩嗶哩面試題+Spring原始碼合集+Java架構實戰電子書,
需要的朋友可以點擊:這個!點這個,暗號:csdn,


轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/229146.html
標籤:其他
上一篇:本文涵蓋Spring整體架構大綱+Spring原始碼分析筆記
下一篇:nginx反向代理配置去除前綴
