CAS原子操作底層原理

搶紅包的問題

本篇我只想講這一個方法,因爲其他的CAS操作類似,只要把這個搞懂了,其他的就不是問題。舉個最簡單的例子,1000個線程要去修改一個值,但是這個值只能被修改一次,比如1000個人搶1個紅包,但是紅包就只有一個:

public class NoAtomicTest {
    private static int money = 1;//紅包
    public static void main(String[] args) {
        Thread[] persons = new Thread[1000];
        for (int i = 0; i < 1000; i++) {

            persons[i] = new Thread(() -> {

                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if (money == 1) {
                    money = 0;
                    System.out.println(Thread.currentThread().getName() + "搶到紅包");
                }

            },"會員"+i);
        }
        for (int i = 0; i < persons.length; i++) {
            persons[i].start();
        }
    }
}

輸出可能是:

會員0搶到紅包
會員5搶到紅包
會員1搶到紅包

可以看到一個紅包居然可以三個人搶到,就有問題啦,現在我們用原子操作試試:

public class AtomicTest {

    private static AtomicInteger money = new AtomicInteger(1);

    public static void main(String[] args) {
        Thread[] persons = new Thread[1000];
        for (int i = 0; i < 1000; i++) {

            persons[i] = new Thread(() -> {

                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if (money.compareAndSet(1, 0)) {
                    System.out.println(Thread.currentThread().getName() + "搶到紅包");
                }

            }, "會員" + i);
        }
        for (int i = 0; i < persons.length; i++) {
            persons[i].start();
        }
    }
}

結果永遠都是1個人搶到。

compareAndSet

例子舉完了,我們得知道爲什麼用這個方法可以避免問題,我們要看看這個原子類AtomicInteger的一些源碼:

   public final boolean compareAndSet(int expectedValue, int newValue) {
        return U.compareAndSetInt(this, VALUE, expectedValue, newValue);
    }

先不管參數,會發現是調用UcompareAndSetInt,我們看看U是什麼:

private static final jdk.internal.misc.Unsafe U = jdk.internal.misc.Unsafe.getUnsafe();
 public static Unsafe getUnsafe() {
        return theUnsafe;
    }
 private static final Unsafe theUnsafe = new Unsafe();

原來是內部的Unsafe 對象,這個方法簡單來說就是可以操作底層硬件的,可以直接用C/C++語言,可以使用彙編語言的哦。
在這裏插入圖片描述

objectFieldOffset

然後我們發現還有個VALUE哪裏來的:

private static final long VALUE = U.objectFieldOffset(AtomicInteger.class, "value");
public long objectFieldOffset(Class<?> c, String name) {
        if (c == null || name == null) {
            throw new NullPointerException();
        }

        return objectFieldOffset1(c, name);
    }
private native long objectFieldOffset1(Class<?> c, String name);

原來是Unsafe的方法獲得的,看方法一是就是說獲得對象某個屬性的偏移,也就是value屬性的偏移。其實就是內存中這個屬性的地址啦,你可以把內存地址理解成一個數組,value屬性就在數組裏,你要獲取是不是得有索引啊,索引就是相當於偏移,當然物理內存中不是那麼簡單存儲。其實最後是調用了本地方法,我們來看看這個的方法到底是在什麼地方,我下了JDK11的源碼:
在這裏插入圖片描述
是這句:
在這裏插入圖片描述
其實就是個宏定義,指向方法:
在這裏插入圖片描述
內部又調用了:
在這裏插入圖片描述
一堆C++的代碼,其實我也太懂,我猜是調用了JavaFieldStreamoffset方法來獲得偏移量,這個是JavaFieldStream父類FieldStreamBase的方法,而且還有其他的方法:
在這裏插入圖片描述
內部是調用了FieldInfo的方法:
在這裏插入圖片描述
在這裏插入圖片描述
核心的應該是這句,其實就是把兩個unsigned short拼成一個int,然後右移2位:

 return build_int_from_shorts(_shorts[low_packed_offset], _shorts[high_packed_offset]) >> FIELDINFO_TAG_SIZE;

16位低位和16位高位(low_packed_offsethigh_packed_offset)合起來成一個32位:

inline int build_int_from_shorts( jushort low, jushort high ) {
  return ((int)((unsigned int)high << 16) | (unsigned int)low);
}

low_packed_offsethigh_packed_offset這兩個參數哪裏來的呢,應該是初始化的時候設置好的:
在這裏插入圖片描述
其他的我就不多說啦,你只要知道這個偏移量是可以從底層的屬性的偏移量中獲取的。

compareAndSetInt

最後還是本地方法:

 @HotSpotIntrinsicCandidate
    public final native boolean compareAndSetInt(Object o, long offset,
                                                 int expected,
                                                 int x);

這個的意思就是說針對某個對象o,根據偏移量offset找到屬性值,跟我們所期望的值expected是否一樣,如果一樣,就把這個屬性值設置成x,返回true,否則false。
那我們看看本地方法吧:
在這裏插入圖片描述
同樣調用的是Unsafe_CompareAndSetInt
在這裏插入圖片描述
對應的就是access.hpp的:
在這裏插入圖片描述
在這裏插入圖片描述
具體的實現應該就是比如windows上的atomic_windows_x86.hppAtomic::PlatformCmpxchg方法:

template<>
template<typename T>
inline T Atomic::PlatformCmpxchg<4>::operator()(T exchange_value,
                                                T volatile* dest,
                                                T compare_value,
                                                atomic_memory_order order) const {
  STATIC_ASSERT(4 == sizeof(T));
  // alternative for InterlockedCompareExchange
  __asm {
    mov edx, dest //把屬性地址放進寄存器edx
    mov ecx, exchange_value //把新的屬性值放進寄存器ecx
    mov eax, compare_value //屬性的的期望值
    //取寄存器edx中的值所對應的內存地址中的值和eax中的值作比較,如果相等就設置成ecx寄存器中的值。而且還用lock上鎖了,要麼就是總線鎖,要麼就是緩存鎖.
    //[edx]屬於間接尋址,就是獲取到寄存器edx中的值(內存地址)後再去內存中取值
    //dword ptr 雙字的指針指向內存地址
    lock cmpxchg dword ptr [edx], ecx 
  }
}

好了,今天就到這裏了,希望對學習理解有幫助,大神看見勿噴,僅爲自己的學習理解,能力有限,請多包涵。

發佈了153 篇原創文章 · 獲贊 46 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章