徹底掌握Java CAS自旋鎖原理 彙編底層源碼

cas典型使用場景

如果多個處理器同時對共享變量進行讀改寫(i++就是經典的讀改寫操作)操作,那麼共享變量就會被多個處理器同時進行操作,這樣讀改寫操作就不是原子的,操作完之後共享變量的值會和期望的不一致,舉個例子:如果i=0,我們進行兩次i++操作,我們期望的結果是2,但是有可能結果是1。如下圖
在這裏插入圖片描述
原因是有可能多個處理器同時從各自的緩存中讀取變量i,分別進行+1操作,然後分別寫入系統內存當中。那麼想要保證讀改寫共享變量的操作是原子的,就必須保證CPU1讀改寫共享變量的時候,CPU2不能操作緩存了該共享變量內存地址的緩存。
我們有很多種解決方法,比如用原子類解決:

// jdk5 之後的原子類
AtomicInteger atomicInteger = new AtomicInteger();
atomicInteger.incrementAndGet();

AtomicInteger.incrementAndGet其中就用到了cas算法。
cas算法基本流程就是1,獲取舊的值 2,計算 3 寫入值之前拿舊的值與最新的值比較,若相等說明未被他他線程改變,可以執行寫入操作。否則循環進行以上操作。看完流程圖後,我們一步步查看源碼。

在這裏插入圖片描述

爲了便於理解cas,我們場景進行場景模擬

兩個線程(cpu1,cpu2)分別執行atomicInteger.incrementAndGet()

場景A cpu1執行過程中,內存中的值未被其他cpu2線程修改

cpu1讀取內存當前值 :oldValue = 0

cpu1計算:directValue = 0+1=1

cpu1讀取內存當前值 :nowValue=0(內存中的值未被其他線程修改)

因爲oldValue =nowValue所以cpu1將內存中的值更新爲directValue

場景B cpu1執行過程中,內存中的值被cpu2線程修改

cpu1讀取內存當前值 :oldValue = 0

cpu1計算:directValue = 0+1=1

cpu1讀取內存當前值 :nowValue=1(內存中的值已經被cpu2修改爲1)

因爲oldValue 不等於nowValue所以cpu1進行自旋操作 cpu1重新讀取內存當前值 :oldValue = 1

cpu1計算:directValue = 1+1=2

cpu1讀取內存當前值 :nowValue=1(cpu2執行結束,內存中的值未被其他線程修改)

因爲oldValue =nowValue所以cpu1將內存中的值更新爲directValue(2) 因次最終,值爲正確的值2

場景C 同時進行內存寫入

假設time1=time2代表同一時刻。

cpu1讀取內存當前值 :oldValue = 0

cpu1計算:directValue = 0+1=1

cpu1讀取內存當前值 :nowValue=0(內存中的值未被其他線程修改)

在time1時刻,因爲oldValue =nowValue所以cpu1將內存中的值更新爲directValue

cpu2讀取內存當前值 :oldValue = 0

cpu2計算:directValue = 0+1=1

cpu2讀取內存當前值 :nowValue=0(內存中的值未被其他線程修改)

time2時刻因爲oldValue =nowValue所以cpu2將內存中的值更新爲directValue

即兩個線程同一時刻進行內存寫入。這時候cas是怎麼保證結果正確呢?下文會做出解釋。

其他場景:ABA 問題
ABA問題大概理解你的女朋友在離開你的這段兒時間經歷了別的人,自旋就是你空轉等待,一直等到她接納你爲止。

有這麼一種場景:oldValue=0,nowValue被cpu2改爲1,又被cpu2或其他線程改成了0。雖然oldValue=nowValue=0,但是這個0已經不是之前的0了,但是cpu1無感知,於是進行寫入操作。

並不是這種場景一定會給程序結果帶來影響。若有影響解決辦法是添加版本號(AtomicStampedReference)。

大家應該比較清晰了,接下來我們進行源碼講解

第一步 查看java代碼AtomicInteger類的incrementAndGet()方法。

public final int incrementAndGet() {
        for (;;) {
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
                return next;
        }
    }

public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

第二步 java代碼incrementAndGet調用了unsafe類的compareAndSwapInt方法
Unsafe類是rt.jar包中的類,它提供了原子級別的操作,它的方法都是native方法,通過JNI訪問本地的C++庫。

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

第三步 查看native compareAndSwapInt方法
native方法的具體實現是用C語言實現的,因爲jdk就是用C語言編寫的。當有一些需要和硬件打交道的方法,java是做不了的,於是它就偷懶聲明一個native方法讓c去寫一個方法去和硬件打交道,c寫好之後java直接調用即可。
native 方法無法從jdk中看只能查看jvm
jvm是一個標準,它的實現有很多

  • 開源:Openjdk
  • Sun:Hotspot
  • IBM:OpenJ9
  • 阿里巴巴:Alibaba JVM
  • more

依照現在的授權,JVM的源碼可以放在OpenJDK裏提供。
這裏我給出了OpenJDK源碼下載鏈接
鏈接:https://pan.baidu.com/s/1ENcBozn3HEVvZ1f1xlwhDw
提取碼:ye04
下載解壓即可

unsafe.cpp所在目錄爲:
openjdk/hotspot/src/share/vm/prims/unsafe.cpp

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

可以看出返回的是(jint)(Atomic::cmpxchg(x, addr, e)) 跳轉第四步

第四步 查看Atomic::cmpxchg(x, addr, e)方法
此方法在atomic_linux_x86.inline.hpp的第93行
atomic_linux_x86.inline.hpp的目錄爲:
openjdk/hotspot/src/os_cpu/linux_x86/vm/atomic_linux_x86.inline.hpp

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;
}

inline內聯方法,解決一些頻繁調用的函數大量消耗棧空間(棧內存)的問題
is_MP代表是否多核處理器,源碼請跳轉第五步
實現爲
__asm__ volatile ( C語言內嵌彙編)
LOCK_IF_MP(彙編指令:如果多個cpu則進行鎖定,源碼請跳轉第六
cmpxchgl (彙編指令:compile and exchange硬件直接支持)

第五步 查看is_MP()方法
is_MP()方法在 os.hpp中
os.hpp的目錄爲:
openjdk/hotspot/src/share/vm/runtime/os.hpp

  static inline bool is_MP() {
    // During bootstrap if _processor_count is not yet initialized
    // we claim to be MP as that is safest. If any platform has a
    // stub generator that might be triggered in this phase and for
    // which being declared MP when in fact not, is a problem - then
    // the bootstrap routine for the stub generator needs to check
    // the processor count directly and leave the bootstrap routine
    // in place until called after initialization has ocurred.
    return (_processor_count != 1) || AssumeMP;
  }

第六步 查看LOCK_IF_MP方法
LOCK_IF_MP()方法在atomic_linux_x86.inline.hpp
atomic_linux_x86.inline.hpp目錄爲:
openjdk/hotspot/src/os_cpu/linux_x86/vm/atomic_linux_x86.inline.hpp

#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "

綜上,它的最終實現爲一條指定

lock cmpxchg

cmpxchg 不具有原子性,lock指令在執行後面指令的時候鎖定一個北橋電信號,當執行cmpxchg 其他cpu不允許做修改,所以lock cmpxchg具有原子性。
所以cas還是會上鎖,不過鎖定北橋信號(不採用鎖總線的方式)比鎖定總線輕量,這就很好的解釋了場景C同時寫入問題。

下一期文章
synchronized與volatile的硬件級實現
如果覺得寫的可以的話,能不能掃碼關注公衆號“雲計算平臺技術”呢?
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章