Java 併發包原子操作類解析

神祕傑克發表於2022-01-10

Java 併發包原子操作類解析

前言

JUC 包中提供了一些列原子操作類,這些類都是使用非阻塞演算法CAS實現的,相比使用鎖實現原子性操作在效能上有較大提高。

由於原子性操作的原理都大致相同,本文只講解簡單的 AtomicLong 類的原理以及在JDK8中新增的 LongAdder 類原理。

原子變數操作類

JUC 併發包中包含 AtomicInteger、AtomicLong 和 AtomicBoolean 等原子性操作類,原理大致類似,接下來我們看一下 AtomicLong 類。

AtomicLong 是原子性遞增或者遞減類,內部使用Unsafe來實現,我們看下面的程式碼。

public class AtomicLong extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 1927816293512124184L;
    //1. 獲取Unsafe例項
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    //2. 存放變數value的偏移量
    private static final long valueOffset;
    //3. 判斷JVM是否支援Long型別無鎖CAS
    static final boolean VM_SUPPORTS_LONG_CAS = VMSupportsCS8();

    private static native boolean VMSupportsCS8();

    static {
        try {
            //4. 獲取value在AtomicLong中的偏移量
            valueOffset = unsafe.objectFieldOffset
                (AtomicLong.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    //5. 實際變數值
    private volatile long value;

    public AtomicLong(long initialValue) {
        value = initialValue;
    }
   ......
}

首先通過Unsafe.getUnsafe()方法獲取到 Unsafe 類的例項,

為什麼可以獲取到 Unsafe 類的例項?因為 AtomicLong 類也在 rt.jar 包下,所以可以通過 BootStrap 類載入器進行載入。

第二步、第四步獲取 value 變數在 AtomicLong 類中的偏移量。

第五步的 value 變數被宣告為了volatile,這是為了在多執行緒下保證記憶體可見性,而 value 儲存的就是具體計數器的值。

遞增和遞減操作程式碼

接下來我們看一下 AtomicLong 中的主要函式。

//呼叫unsafe方法,原子性設定value值為原始值+1,返回遞增後的值
public final long incrementAndGet() {
        return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L;
}
//呼叫unsafe方法,原子性設定value值為原始值-1,返回值遞減後的值
public final long decrementAndGet() {
        return unsafe.getAndAddLong(this, valueOffset, -1L) - 1L;
}
//呼叫unsafe方法,原子性設定value值為原始值+1,返回原始值
public final long getAndIncrement() {
        return unsafe.getAndAddLong(this, valueOffset, 1L);
}
//呼叫unsafe方法,原子性設定value值為原始值-1,返回原始值
public final long getAndDecrement() {
        return unsafe.getAndAddLong(this, valueOffset, -1L);
}

上述程式碼都是通過呼叫 Unsafe 的getAndAddLong()方法來實現操作,這個函式是一個原子性操作,第一個引數為 AtomicLong 例項的引用,第二個引數是 value 變數在 AtomicLong 中的偏移值,第三個引數是要設定的第二個變數的值。

其中,getAndIncrement()方法在JDK1.7中的實現邏輯如下。

public final long getAndIncrement() {
   while (true) {
        long current = get();
        long next = current + 1;
        if (compareAndSet(current,next))
            return current;
    }
}

這段程式碼中,每個執行緒都是拿到變數的當前值(因為 value 是 volatile 變數,所以拿到的都是最新的值),然後在工作記憶體中進行增加 1 操作,之後使用CAS修改變數的值。如果設定失敗,則一直迴圈嘗試,直到設定成功。

JDK8中的邏輯為:

public final long getAndAddLong(Object var1, long var2, long var4) {
        long var6;
        do {
            var6 = this.getLongVolatile(var1, var2);
        } while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4));

        return var6;
}

可以看到,JDK1.7的 AtomicLong 中的迴圈邏輯已經被JDK8中的原子操作類 Unsafe 內建了,之所以內建應該是考慮到這個函式在其他地方也會用到,而內建可以提高複用性

compareAndSet(long expect, long update)方法

public final boolean compareAndSet(long expect, long update) {
        return unsafe.compareAndSwapLong(this, valueOffset, expect, update);
}

如上程式碼我們可以知道,在內部還是呼叫了unsafe.compareAndSwapLong方法。如果原子變數中的 value 值等於 expect,則使用 update 值更新該值並返回 true,否則返回 false。

下面我們通過一個多執行緒使用 AtomicLong 統計 0 的個數的例子來加深理解。

/**
 * @author 神祕傑克
 * 公眾號: Java菜鳥程式設計師
 * @date 2022/1/4
 * @Description 統計0的個數
 */
public class AtomicTest {

    private static AtomicLong atomicLong = new AtomicLong();
    private static Integer[] arrayOne = new Integer[]{0, 1, 2, 3, 0, 5, 6, 0, 56, 0};
    private static Integer[] arrayTwo = new Integer[]{10, 1, 2, 3, 0, 5, 6, 0, 56, 0};

    public static void main(String[] args) throws InterruptedException {
        final Thread threadOne = new Thread(() -> {
            final int size = arrayOne.length;
            for (int i = 0; i < size; ++i) {
                if (arrayOne[i].intValue() == 0) {
                    atomicLong.incrementAndGet();
                }
            }
        });
        final Thread threadTwo = new Thread(() -> {
            final int size = arrayTwo.length;
            for (int i = 0; i < size; ++i) {
                if (arrayTwo[i].intValue() == 0) {
                    atomicLong.incrementAndGet();
                }
            }
        });
        threadOne.start();
        threadTwo.start();
        //等待執行緒執行完畢
        threadOne.join();
        threadTwo.join();
        System.out.println("count總數為: " + atomicLong.get()); //count總數為: 7

    }
}

這段程式碼很簡單,就是每找到一個 0 就會呼叫 AtomicLong 的原子性遞增方法。

在沒有原子類的時候,實現計數器需要一定的同步措施,例如 synchronized 關鍵字等,但這些都是阻塞演算法,對效能有一定的影響,而我們使用的 AtomicLong 使用的是CAS 非阻塞演算法,效能更好。

但是在高併發下,AtomicLong 還會存在效能問題,JDK8 提供了一個在高併發下效能更好的 LongAdder 類。

LongAdder 介紹

前面說過,在高併發下使用 AtomicLong 時,大量執行緒會同時競爭同一個原子變數,但是由於同時只有一個執行緒的 CAS 操作會成功,所以會造成大量執行緒競爭失敗後,會無限迴圈不斷的自旋嘗試 CAS 操作,白白浪費 CPU 資源。

所以在 JDK8 中新增了一個原子性遞增或者遞減類 LongAdder 用來克服高併發 AtomicLong 的缺點。既然 AtomicLong 的效能瓶頸是多個執行緒競爭一個變數的更新產生的,那如果把一個變數分成多個變數,讓多個執行緒競爭多個資源,是不是就解決效能問題了?是的,LongAdder 就是這個思路。

LongAdder原理

如上圖,在使用 LongAdder 時,則是在內部維護多個 Cell 變數,每個 Cell 裡面有一個初始值為 0 的 long 型變數,這樣的話在同等併發量的情況下,爭奪單個執行緒更新操作的執行緒會減少,也就變相的減少爭奪共享資源的併發量。

另外,如果多個執行緒在爭奪同一個 Cell 原子變數時失敗了,它並不會一直自旋重試,而是去嘗試其它 Cell 變數進行 CAS 嘗試,這樣就增加了當前執行緒重試 CAS 成功的可能性,最後,在獲取 LongAdder 當前值時,是把所有的Cell變數的value值累加後再加上base返回的

LongAdder 維護了一個延遲初始化的原子性更新陣列(預設情況下 Cell 陣列是 null)和一個基值變數 base,在一開始時並不建立 Cells 陣列,而是在使用時建立,也就是惰性載入

在一開始判斷 Cell 陣列是 null 並且併發執行緒減少時,所有的累加都是在 base 變數上進行的,保持 Cell 陣列的大小為 2 的 N 次方,在初始化時 Cell 陣列中的 Cell 元素個數為 2,陣列裡面的變數實體是 Cell 型別。Cell 型別是 AtomicLong 的一個改進,用來減少快取的爭用,也就是解決偽共享問題。

在多個執行緒併發修改一個快取行中的多個變數時,由於只能同時有一個執行緒去操作快取行,將會導致效能的下降,這個問題就稱之為偽共享

一般而言,快取行有 64 位元組,我們知道一個 long 是 8 個位元組,填充 5 個 long 之後,一共就是 48 個位元組。

而 Java 中物件頭在 32 位系統下佔用 8 個位元組,64 位系統下佔用 16 個位元組,這樣填充 5 個 long 型即可填滿 64 位元組,也就是一個快取行。

JDK8 以及之後的版本 Java 提供了sun.misc.Contended 註解,通過@Contented 註解就可以解決偽共享的問題。

使用@Contented 註解後會增加 128 位元組的 padding,並且需要開啟-XX:-RestrictContended選項後才能生效。

在 LongAdder 中解決偽共享的真正的核心就在Cell陣列,Cell陣列使用了@Contented註解。

對於大多數孤立的多個原子操作進行位元組填充是浪費的,因為原子性操作都是無規律地分散在記憶體中的(也就是說多個原子性變數的記憶體地址是不連續的),多個原子變數被放入同一個快取行的可能性很小。但是原子性陣列元素的記憶體地址是連續的,所以陣列內的多個元素能經常共享快取行,因此這裡使用@Contented註解對 Cell 類進行位元組填充,這防止了陣列中多個元素共享一個快取行,在效能上是一個提升。

LongAdder 原始碼分析

問題:

  1. LongAdder 的結構是怎樣的?
  2. 當前執行緒應該訪問 Cell 陣列裡面的哪一個 Cell 元素?
  3. 如何初始化 Cell 陣列?
  4. Cell 陣列如何擴容?
  5. 執行緒訪問分配的 Cell 元素有衝突後如何處理?
  6. 如何保證執行緒操作被分配的 Cell 元素的原子性?

接下來我們看一下 LongAdder 的結構:

LongAdder 類繼承自 Striped64 類,在 Striped64 內部維護這三個變數。

  • LongAdder 的真實值其實是 base 的值與 Cell 陣列裡面所有 Cell 元素中的 value 值的累加,base 是個基礎值,預設為 0。
  • cellsBusy 用來實現自旋鎖,狀態值只有 0 和 1,當建立 Cell 元素,擴容 Cell 陣列或者初始化 Cell 陣列時,使用 CAS 操作該變數來保證同時只有一個執行緒可以進行其中之一的操作。
transient volatile Cell[] cells;
transient volatile long base;
transient volatile int cellsBusy;
public class LongAdder extends Striped64 implements Serializable {

Cell 的構造

下面我們看一下 Cell 的構造。

@sun.misc.Contended static final class Cell {
    volatile long value;
    Cell(long x) { value = x; }
    final boolean cas(long cmp, long val) {
        return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
    }

    // Unsafe mechanics
    private static final sun.misc.Unsafe UNSAFE;
    private static final long valueOffset;
    static {
        try {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class<?> ak = Cell.class;
            valueOffset = UNSAFE.objectFieldOffset
                (ak.getDeclaredField("value"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }
}

可以看到,內部維護一個被宣告為 volatile 的變數,這裡宣告 volatile 是為了保證記憶體可見性。另外 cas 函式通過 CAS 操作,保證了當前執行緒更新時被分配的 Cell 元素中 value 值的原子性。並且可以看到 Cell 類是被@Contended 修飾的,避免偽共享。

至此我們已經知道了問題 1、6 的答案了。

sum()

sum()方法返回當前的值,內部操作是累加所有 Cell 內部的 value 值然後在累加 base。

由於計算總合時沒有對 Cell 陣列進行加鎖,所以在累加過程中可能有其它執行緒對 Cell 值進行修改,也可能擴容,所以 sum 返回的值並不是非常準確的,其返回值並不是一個呼叫 sum()方法時原子快照值。

public long sum() {
    Cell[] as = cells; Cell a;
    long sum = base;
    if (as != null) {
        for (int i = 0; i < as.length; ++i) {
            if ((a = as[i]) != null)
                sum += a.value;
        }
    }
    return sum;
}

reset()

reset()方法為重置操作,將 base 設定為 0,如果 Cell 陣列有元素,則元素被重置為 0。

public void reset() {
    Cell[] as = cells; Cell a;
    base = 0L;
    if (as != null) {
        for (int i = 0; i < as.length; ++i) {
            if ((a = as[i]) != null)
                a.value = 0L;
        }
    }
}

sumThenReset()

sumThenReset()方法是 sum()方法的改造版本,該方法在使用 sum 累加對應的 Cell 值後,把當前的 Cell 和 base 重置為 0。

該方法存在線上程安全問題,比如第一個呼叫執行緒清空 Cell 的值,則後一個執行緒呼叫時累加的都是 0 值。
public long sumThenReset() {
    Cell[] as = cells; Cell a;
    long sum = base;
    base = 0L;
    if (as != null) {
        for (int i = 0; i < as.length; ++i) {
            if ((a = as[i]) != null) {
                sum += a.value;
                a.value = 0L;
            }
        }
    }
    return sum;
}

add(long x)

接下來我們主要看 add()方法,這個方法裡面可以回答剛才其他的問題。

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

final boolean casBase(long cmp, long val) {
        return UNSAFE.compareAndSwapLong(this, BASE, cmp, val);
}

該方法首先判斷 cells 是否 null,如果為 null 則在 base 上進行累加。如果 cells 不為 null,或者執行緒執行程式碼 cas 失敗,則去執行第二步。程式碼第二步第三步決定當前執行緒應該訪問 cells 陣列中哪一個 Cell 元素,如果當前執行緒對映的元素存在的話則執行程式碼四。

第四步主要使用 CAS 操作去更新分配的 Cell 元素的 value 值,如果當前執行緒對映的元素不存在或者存在但是 CAS 操作失敗則執行程式碼五。

執行緒應該訪問 cells 陣列的哪一個 Cell 元素是通過 getProbe() & m 進行計算的,其中 m 是當前 cells 陣列元素個數-1,getProbe()則用於獲取當前執行緒中變數 threadLocalRandomProbe 的值,這個值一開始為 0,在程式碼第五步裡面會對其進行初始化。並且當前執行緒通過分配的 Cell 元素的 cas 函式來保證對 Cell 元素 value 值更新的原子性。

現在我們已經明白了第二個問題。

下面我們看一下 longAccumulate(x,null,uncontended)方法,該方法主要是 cells 陣列初始化和擴容的地方。

final void longAccumulate(long x, LongBinaryOperator fn,
                          boolean wasUncontended) {
    //6. 初始化當前執行緒變數ThreadLocalRandomProbe的值
    int h;
    if ((h = getProbe()) == 0) {
        ThreadLocalRandom.current(); // force initialization
        h = getProbe();
        wasUncontended = true;
    }
    boolean collide = false;                // True if last slot nonempty
    for (;;) {
        Cell[] as; Cell a; int n; long v;
        //7.
        if ((as = cells) != null && (n = as.length) > 0) {
            //8.
            if ((a = as[(n - 1) & h]) == null) {
                if (cellsBusy == 0) {       // Try to attach new Cell
                    Cell r = new Cell(x);   // Optimistically create
                    if (cellsBusy == 0 && casCellsBusy()) {
                        boolean created = false;
                        try {               // Recheck under lock
                            Cell[] rs; int m, j;
                            if ((rs = cells) != null &&
                                (m = rs.length) > 0 &&
                                rs[j = (m - 1) & h] == null) {
                                rs[j] = r;
                                created = true;
                            }
                        } finally {
                            cellsBusy = 0;
                        }
                        if (created)
                            break;
                        continue;           // Slot is now non-empty
                    }
                }
                collide = false;
            }
            else if (!wasUncontended)       // CAS already known to fail
                wasUncontended = true;      // Continue after rehash
            //9. 當前Cell存在,則執行CAS設定
            else if (a.cas(v = a.value, ((fn == null) ? v + x :
                                         fn.applyAsLong(v, x))))
                break;
            //10. 當前Cell元素個數大於CPU個數
            else if (n >= NCPU || cells != as)
                collide = false;            // At max size or stale
            //11. 是否有衝突
            else if (!collide)
                collide = true;
            //12. 如果當前元素個數沒有達到CPU個數,並且存在衝突則擴容
            else if (cellsBusy == 0 && casCellsBusy()) {
                try {
                    if (cells == as) {      // Expand table unless stale
                      //12.1
                        Cell[] rs = new Cell[n << 1];
                        for (int i = 0; i < n; ++i)
                            rs[i] = as[i];
                        cells = rs;
                    }
                } finally {
                    //12.2
                    cellsBusy = 0;
                }
                //12.3
                collide = false;
                continue;                   // Retry with expanded table
            }
            //13. 為了能夠找到一個空閒的Cell,重新計算hash值,xorshift演算法生成隨機數
            h = advanceProbe(h);
        }
        //14. 初始化Cell陣列
        else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
            boolean init = false;
            try {                           // Initialize table
                if (cells == as) {
                    //14.1
                    Cell[] rs = new Cell[2];
                    //14.2
                    rs[h & 1] = new Cell(x);
                    cells = rs;
                    init = true;
                }
            } finally {
                //14.3
                cellsBusy = 0;
            }
            if (init)
                break;
        }
        else if (casBase(v = base, ((fn == null) ? v + x :
                                    fn.applyAsLong(v, x))))
            break;                          // Fall back on using base
    }
}

該方法較為複雜,我們主要關注問題 3,問題 4,問題 5。

  1. 如何初始化 Cell 陣列?
  2. Cell 陣列如何擴容?
  3. 執行緒訪問分配的 Cell 元素有衝突後如何處理?

每個執行緒第一次執行到程式碼第六步的時候,會初始化當前執行緒變數 ThreadLocalRandomProbe 的值,該值主要為了計算當前執行緒為了分配到cells陣列中的哪一個cell元素中

cells 陣列的初始化是在程式碼第十四步中進行,其中 cellsBusy 是一個標識,為 0 說明當前 cells 陣列沒有被初始化或者擴容,也沒有在新建 Cell 元素,為 1 說明 cells 陣列正在被初始化或者擴容、建立新元素,通過 CAS 來進行 0 或 1 狀態切換,呼叫的是casCellsBusy()

假設當前執行緒通過 CAS 設定 cellsBuys 為 1,則當前執行緒開始初始化操作,那麼這時候其他執行緒就不能進行擴容了,如程式碼(14.1)初始化 cells 陣列個數為 2,然後使用h & 1計算當前執行緒應該訪問 cell 陣列的那個位置,使用的 h 就是當前執行緒的 threadLocalRandomProbe 變數。然後標識 Cells 陣列以及被初始化,最後(14.3)重置了 cellsBusy 標記。雖然這裡沒有使用CAS操作,但是卻是執行緒安全的,原因是cellsBusy是volatile型別的,保證了記憶體可見性。在這裡初始化的 cells 陣列裡面的兩個元素的值目前還是 null。現在我們知道了問題 3 的答案。

而 cells 陣列的擴容是在程式碼第十二步進行的,對 cells 擴容是有條件的,也就是第十步、十一步條件都不滿足後進行擴容操作。具體就是當前 cells 的元素個數小於當前機器 CPU 個數並且當前多個執行緒訪問了 cells 中同一個元素,從而導致某個執行緒 CAS 失敗才會進行擴容。

為何要涉及 CPU 個數呢?只有當每個 CPU 都執行一個執行緒時才會使多執行緒的效果最佳,也就是當 cells 陣列元素個數與 CPU 個數一致時,每個 Cell 都使用一個 CPU 進行處理,這時效能才是最佳的。

程式碼第十二步也是先通過 CAS 設定 cellsBusy 為 1,然後才能進行擴容。假設 CAS 成功則執行程式碼(12.1)將容量擴充為之前的 2 倍,並複製 Cell 元素到擴容後陣列。另外,擴容後 cells 陣列裡面除了包含複製過來的元素外,還包含其他新元素,這些元素的值目前還是 null。現在我們知道了問題 4 的答案。

在程式碼第七步、第八步中,當前執行緒呼叫 add()方法並根據當前執行緒的隨機數 threadLocalRandomProbe 和 cells 元素個數計算要訪問的 Cell 元素下標,然後如果發現對應下標元素的值為 null,則新增一個 Cell 元素到 cells 陣列,並且在將其新增到 cells 陣列之前要競爭設定 cellsBusy 為 1。

而程式碼第十三步,對 CAS 失敗的執行緒重新計算當前執行緒的隨機值 threadLocalRandomProbe,以減少下次訪問 cells 元素時的衝突機會。這裡我們就知道了問題 5 的答案。

總結

該類通過內部 cells 陣列分擔了高併發下多執行緒同時對一個原子變數進行更新時的競爭量,讓多個執行緒可以同時對 cells 陣列裡面的元素進行並行的更新操作。另外,陣列元素 Cell 使用@Contended 註解進行修飾,這避免了 cells 陣列內多個原子變數被放入同一個快取行,也就是避免了偽共享。

LongAccumulator 相比於 LongAdder,可以為累加器提供非 0 的初始值,後者只能提供預設的 0 值。另外,前者還可以指定累加規則,比如不進行累加而進行相乘,只需要在構造 LongAccumulator 時傳入自定義的雙目運算器即可,後者則內建累加的規則。

相關文章