CAS原子操作史上最底層原理
- 搶紅包的問題
- compareAndSet
-
- objectFieldOffset
- compareAndSetInt
搶紅包的問題
本篇我隻想講這一個方法,因為其他的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
位:
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
}
}
好了,今天就到這裡了,希望對學習了解有幫助,大神看見勿噴,僅為自己的學習了解,能力有限,請多包涵。