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);
}
先不管參數,會發現是調用U
的compareAndSetInt
,我們看看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++的代碼,其實我也太懂,我猜是調用了JavaFieldStream
的offset
方法來獲得偏移量,這個是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_offset
和high_packed_offset
)合起來成一個32位:
inline int build_int_from_shorts( jushort low, jushort high ) {
return ((int)((unsigned int)high << 16) | (unsigned int)low);
}
low_packed_offset
和high_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.hpp
的Atomic::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
}
}
好了,今天就到這裏了,希望對學習理解有幫助,大神看見勿噴,僅爲自己的學習理解,能力有限,請多包涵。