深入分析 Java 樂觀鎖 前言 樂觀鎖是什麼? 樂觀鎖的實現 LongAdder vs AtomicLong 代碼

前言

激烈的鎖競爭,會造成線程阻塞掛起,導致系統的上下文切換,增加系統的性能開銷。那有沒有不阻塞線程,且保證線程安全的機制呢?——樂觀鎖

樂觀鎖是什麼?

操作共享資源時,總是很樂觀,認爲自己可以成功。在操作失敗時(資源被其他線程佔用),並不會掛起阻塞,而僅僅是返回,並且失敗的線程可以重試。

優點:

  • 不會死鎖
  • 不會飢餓
  • 不會因競爭造成系統開銷

樂觀鎖的實現

CAS 原子操作

CAS。在 java.util.concurrent.atomic 中的類都是基於 CAS 實現的。

以 AtomicLong 爲例,一段測試代碼:

@Test
public void testCAS() {
    AtomicLong atomicLong = new AtomicLong();
    atomicLong.incrementAndGet();
}

java.util.concurrent.atomic.AtomicLong#incrementAndGet 的實現方法是:

public final long incrementAndGet() {
    return U.getAndAddLong(this, VALUE, 1L) + 1L;
}

其中 U 是一個 Unsafe 實例。

private static final jdk.internal.misc.Unsafe U = jdk.internal.misc.Unsafe.getUnsafe();

本文使用的源碼是 JDK 11,其 getAndAddLong 源碼爲:

@HotSpotIntrinsicCandidate
public final long getAndAddLong(Object o, long offset, long delta) {
    long v;
    do {
        v = getLongVolatile(o, offset);
    } while (!weakCompareAndSetLong(o, offset, v, v + delta));
    return v;
}

可以看到裏面是一個 while 循環,如果不成功就一直循環,是一個樂觀鎖,堅信自己能成功,一直 CAS 直到成功。最終調用了 native 方法:

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

處理器實現原子操作

從上面可以看到,CAS 是調用處理器底層的指令來實現原子操作,那麼處理器底層是如何實現原子操作的呢?

處理器的處理速度>>處理器與物理內存的通信速度,所以在處理器內部有 L1、L2 和 L3 的高速緩存,可以加快讀取的速度。

單核處理器能夠保存內存操作是原子性的,當一個線程讀取一個字節,所以進程和線程看到的都是同一個緩存裏的字節。但是多核處理器裏,每個處理器都維護了一塊字節的內存,每個內核都維護了一個字節的緩存,多線程併發會存在緩存不一致的問題。

那處理器如何保證內存操作的原子性呢?

  • 總線鎖定:當處理器要操作共享變量時,會在總線上發出 Lock 信號,其他處理器就不能操作這個共享變量了。
  • 緩存鎖定:某個處理器對緩存中的共享變量操作後,就通知其他處理器重新讀取該共享資源。

LongAdder vs AtomicLong

本文分析的 AtomicLong 源碼,其實是在循環不斷嘗試 CAS 操作,如果長時間不成功,就會給 CPU 帶來很大開銷。JDK 1.8 中新增了原子類 LongAdder,能夠更好應用於高併發場景。

LongAdder 的原理就是降低操作共享變量的併發數,也就是將對單一共享變量的操作壓力分散到多個變量值上,將競爭的每個寫線程的 value 值分散到一個數組中,不同線程會命中到數組的不同槽中,各個線程只對自己槽中的 value 值進行 CAS 操作,最後在讀取值的時候會將原子操作的共享變量與各個分散在數組的 value 值相加,返回一個近似準確的數值。

LongAdder 內部由一個base變量和一個 cell[] 數組組成。當只有一個寫線程,沒有競爭的情況下,LongAdder 會直接使用 base 變量作爲原子操作變量,通過 CAS 操作修改變量;當有多個寫線程競爭的情況下,除了佔用 base 變量的一個寫線程之外,其它各個線程會將修改的變量寫入到自己的槽 cell[] 數組中。

一個測試用例:

@Test
public void testLongAdder() {
    LongAdder longAdder = new LongAdder();
    longAdder.add(1);
    System.out.println(longAdder.longValue());
}

先看裏面的 longAdder.longValue() 代碼:

public long longValue() {
    return sum();
}

最終是調用了 sum() 方法,是對裏面的 cells 數組每項加起來求和。這個值在讀取的時候並不準,因爲這期間可能有其他線程在併發修改 cells 中某個項的值:

public long sum() {
    Cell[] cs = cells;
    long sum = base;
    if (cs != null) {
        for (Cell c : cs)
            if (c != null)
                sum += c.value;
    }
    return sum;
}

add() 方法源碼:

public void add(long x) {
    Cell[] cs; long b, v; int m; Cell c;
    if ((cs = cells) != null || !casBase(b = base, b + x)) {
        boolean uncontended = true;
        if (cs == null || (m = cs.length - 1) < 0 ||
            (c = cs[getProbe() & m]) == null ||
            !(uncontended = c.cas(v = c.value, v + x)))
            longAccumulate(x, null, uncontended);
    }
}

add 具體的代碼本篇文章就不詳細敘述了~

代碼

代碼和思維導圖在 GitHub 項目中,歡迎大家 star!

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