在 Java 中不少工具類都在使用 CAS(Compare And Set)用以提高併發的效率以及數據的準確性質。java
對於大部分人來講,最多見的應該就是使用 AtomicXXX、以及在使用 Lock 相關的子類 的時候咱們知道他們的底層運用了 CAS,也知道 CAS 就是傳入一個更新前得期待值(expect)和一個須要更新的值(update),若是知足要求那麼執行更新,不然的話就算執行失敗,來達到數據的原子性。linux
咱們知道 CAS 確定用某一種方式在底層保證了數據的原子性,它的好處是c++
private int value = 0;
public static void main(String[] args) {
Test test = new Test();
test.increment();
System.out.println("期待值:" + 100 * 100 + ",最終結果值:" + test.value);
}
private void increment() {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
for (int j = 0; j < 100; j++) {
value++;
}
}).start();
}
}
複製代碼
輸出:期待值:10000,最終結果值:9900
windows
能夠發現輸出的結果值錯誤,這是由於 value++
不是一個原子操做,它將 value++
拆分紅了 3 個步驟 load、add、store
,多線程併發有可能上一個線程 add 事後尚未 store 下一個線程又執行了 load 了這種重複形成獲得的結果可能比最終值要小。數組
固然在這裏加
volatile int value
也是沒有用的由於 32 位的 int 操做自己就是原子的,並且 volatile 也沒有辦法讓這 3 個操做原子性執行,它只能禁止某個指令重排序來保證其對應的內存可見,若是是long 等 64 位操做類型的能夠加上 volatile
,由於在 32 位的機器上寫操做可能會被分配到不一樣的總線事務上去操做(能夠想象成分紅了 2 步操做,第一步操做前 32 位後一步操做後 32 位),而總線事務的執行是由總線仲裁決定的不能保證它的執行順序(至關於前者加了 32 位可能就切換到其它的地方執行了,好比直接就讀取了,那麼數據的讀取就只讀取到了寫入一半的值)安全
咱們知道關於 CAS 的操做基本上都封裝在 Unsafe 這個包裏面,可是因爲 Unsafe 不容許咱們外部使用,它認爲這是一個不安全的操做,好比若是直接使用 Unsafe unsafe = Unsafe.getUnsafe();
就會拋出 Exception in thread "main" java.lang.SecurityException: Unsafe
。服務器
咱們查看下源代碼,原來是由於它作了校驗多線程
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
複製代碼
因此咱們能夠經過反射來調用它(固然實際操做中不建議這麼使用,此處爲了演示方便)併發
public class Test {
// value 的內存地址,便於直接找到 value
private static long valueOffset = 0;
{
try {
// 這個內存地址是和 value 這個成員變量的值綁定在一塊兒的
valueOffset = getUnsafe().objectFieldOffset
(Test.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private int value;
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
Test test = new Test();
test.increment();
}
private void increment() throws NoSuchFieldException, IllegalAccessException {
Unsafe unsafe = getUnsafe();
for (int i = 0; i < 100; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
unsafe.getAndAddInt(this, valueOffset, 1);
}
}).start();
}
System.out.println("須要獲得的結果爲: " + 100 * 1000);
System.out.println("實際獲得的結果爲: " + value);
}
// 反射獲取 Unsafe
private Unsafe getUnsafe() throws NoSuchFieldException, IllegalAccessException {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
return (Unsafe) field.get(null);
}
}
複製代碼
這下咱們就能從輸出中看到結果是正確的了app
咱們繼續探討, getAndAddInt 調用了 unsafe.compareAndSwapInt(Object obj, long valueOffset, int expect, int update)
這個方法在 Hotspot 究竟是如何實現的,咱們發現調用的是 native 的 unsafe.compareAndSwapInt(Object obj, long valueOffset, int expect, int update)
,咱們翻看 Hotspot 源碼發如今 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
複製代碼
從中咱們能夠看到它是使用了 Atomic::cmpxchg(x, addr, e)
這個操做來完成的,在不一樣的底層硬件會有不同的代碼 Hotspot 向上幫咱們屏蔽了細節。這個實現方法在 solaris,windows,linux_x86 等都有不同的實現方法,咱們用咱們最多見的服務器 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__
表示的是後續是一段彙編代碼volatile
此處的 volatile 和 Java 中的有些區別,這裏使用用以告訴編譯器再也不對這段代碼進行彙編優化LOCK_IF_MP
表示的是若是操做系統是多核的那麼就須要加鎖來保證其原子性cmpxchgl
就是彙編中的比較而且交換從這裏就能看出來,CAS 底層也是在用鎖來保證其原子性的。在 Intel 早期的實現中是直接將總線鎖住,這樣致使其它沒有得到總線事務訪問權的處理器沒法執行後續的操做,性能會極大的下降。
後續 Intel 對其進行了優化升級,在 x86 處理器中能夠只須要鎖定 特定的內存地址,那麼其它處理器也就能夠繼續使用總線來訪問內存數據了,只不過是若是其它總線也要訪問被鎖住的內存地址數據時會阻塞而已,這樣來大幅度的提高了性能。
可是思考一下如下幾點問題的
固然 ABA 的問題可使用增長版本號來控制,每次操做版本號 + 1,版本號變動了說明值就被改過一次了,在 Java 中 AtomicStampedReference 這個類提供了這種問題的解決方案。
而對於說第一個問題來講在 Java8 中也有了對應的優化,Java 8 中提供了一些新的工具類用以解決這種問題,以下
咱們挑一個來看,其它都是相似的
它的原理主要採用CAS分段機制與自動分段遷移機制,最開始是在 base 上面進行 CAS 操做,後續併發線程過多,那麼就將這大量的線程分配到 cells 數組中去,每一個數組的線程單獨去執行累加操做,最終再合併結果
能夠看到跟作直接作同步掛起或者喚醒線程相好比果可以合理的使用 CAS 進行操做的話或者是將其兩者合併使用,那麼在併發性能上可以提高一個量級
參考: JAVA 中的 CAS