JUC包中的分而治之策略-為提高效能而生

阿里云云棲社群發表於2019-01-19

一、前言

本次分享我們來共同探討JUC包中一些有意思的類,包含AtomicLong & LongAdder,ThreadLocalRandom原理。

二、AtomicLong & LongAdder

2.1 AtomicLong 類

AtomicLong是JUC包提供的原子性操作類,其內部通過CAS保證了對計數的原子性更新操作。

大家可以翻看原始碼發現內部是通過UnSafe(rt.jar)這個類的CAs操作來保證對內部的計數器變數 long value進行原子性更新的,比如JDK8中:

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

其中unsafe.getAndAddLong的程式碼如下:

  public final long getAndAddLong(Object paramObject, long paramLong1, long paramLong2)
  {
    long l;
    do
    {
      l = getLongVolatile(paramObject, paramLong1);//(1)
    } while (!compareAndSwapLong(paramObject, paramLong1, l, l + paramLong2));//(2)
    return l;
  }

可知最終呼叫的是native 方法compareAndSwapLong原子性操作。

當多個執行緒呼叫同一個AtomicLong例項的incrementAndGet方法後,多個執行緒都會執行到unsafe.getAndAddLong方法,然後多個執行緒都會執行到程式碼(1)處獲取計數器的值,然後都會去執行程式碼(2),如果多個執行緒同時執行了程式碼(2),由於CAS具有原子性,所以只有一個執行緒會更新成功,然後返回true從而退出迴圈,整個更新操作就OK了。其他執行緒則CAS失敗返回false,則迴圈一次在次從(1)處獲取當前計數器的值,然後在嘗試執行(2),這叫做CAS的自旋操作,本質是使用Cpu 資源換取使用鎖帶來的上下文切換等開銷。

2.2 LongAdder類

AtomicLong類為開發人員使用執行緒安全的計數器提供了方便,但是AtomicLong在高併發下存在一些問題,如上所述,當大量執行緒呼叫同一個AtomicLong的例項的方法時候,同時只有一個執行緒會CAS計數器的值成功,失敗的執行緒則會原地佔用cpu進行自旋轉重試,這回造成大量執行緒白白浪費cpu原地自旋轉。

在JDK8中新增了一個LongAdder類,其採用分而治之的策略來減少同一個變數的併發競爭度,LongAdder的核心思想是把一個原子變數分解為多個變數,讓同樣多的執行緒去競爭多個資源,這樣競爭每個資源的執行緒數就被分擔了下來,下面通過圖形來理解下兩者設計的不同之處:

如上圖AtomicLong是多個執行緒同時競爭同一個原子變數。

如上圖LongAdder內部維護多個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 機制
        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);
            }
        }
    }

LongAdder維護了一個延遲初始化的原子性更新陣列(預設情況下Cell陣列是null)和一個基值變數base,由於Cells佔用記憶體是相對比較大的,所以一開始並不建立,而是在需要時候在建立,也就是惰性 建立。

當一開始判斷cell陣列是null並且併發執行緒較少時候所有的累加操作都是對base變數進行的,這時候就退化為了AtomicLong。cell陣列的大小保持是2的N次方大小,初始化時候Cell陣列的中Cell的元素個數為2,陣列裡面的變數實體是Cell型別。

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

    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;
    }

如上程式碼可知首先把base的值賦值給sum變數,然後通過迴圈把每個cell元素的value值累加到sum變數上,最後返回sum.

其實這是一種分而治之的策略,先把併發量分擔到多個原子變數上,讓多個執行緒併發的對不同的原子變數進行操作,然後獲取計數時候在把所有原子變數的計數和累加。

思考問題:

  • 何時初始化cell陣列
  • 當前執行緒如何選擇cell中的元素進行訪問
  • 如果保證cell中元素更新的執行緒安全
  • cell陣列何時進行擴容,cell元素個數可以無限擴張?

效能對比,這裡有一個文章 http://blog.palominolabs.com/2014/02/10/java-8-performance-improvements-longadder-vs-atomiclong/

三、 Random & ThreadLocalRandom

3.1 Random類原理及其侷限性

在JDK7之前包括現在java.util.Random應該是使用比較廣泛的隨機數生成工具類,下面先通過簡單的程式碼看看java.util.Random是如何使用的:

public class RandomTest {
    public static void main(String[] args) {

        //(1)建立一個預設種子的隨機數生成器
        Random random = new Random();
        //(2)輸出10個在0-5(包含0,不包含5)之間的隨機數
        for (int i = 0; i < 10; ++i) {
            System.out.println(random.nextInt(5));
        }
    }
}
  • 程式碼(1)建立一個預設隨機數生成器,使用預設的種子。
  • 程式碼(2)輸出輸出10個在0-5(包含0,不包含5)之間的隨機數。
    public int nextInt(int bound) {
        //(3)引數檢查
        if (bound <= 0)
            throw new IllegalArgumentException(BadBound);
        //(4)根據老的種子生成新的種子
        int r = next(31);
        //(5)根據新的種子計算隨機數
        ...
        return r;
    } 

如上程式碼可知新的隨機數的生成需要兩個步驟

  • 首先需要根據老的種子計算生成新的種子。
  • 然後根據新的種子和bound變數通過一定的演算法來計算新的隨機數。

下面看下next()程式碼:

    protected int next(int bits) {
        long oldseed, nextseed;
        AtomicLong seed = this.seed;
        do {
            //(6)獲取當前原子變數種子的值
            oldseed = seed.get();
            //(7)根據當前種子值計算新的種子
            nextseed = (oldseed * multiplier + addend) & mask;
            //(8)使用新種子替換老的種子
        } while (!seed.compareAndSet(oldseed, nextseed));
        //(9)
        return (int)(nextseed >>> (48 - bits));
    }
  • 程式碼(6)使用原子變數的get方法獲取當前原子變數種子的值
  • 程式碼(7)根據具體的演算法使用當前種子值計算新的種子
  • 程式碼(8)使用CAS操作,使用新的種子去更新老的種子,多執行緒下可能多個執行緒都同時執行到了程式碼(6)那麼可能多個執行緒都拿到的當前種子的值是同一個,然後執行步驟(7)計算的新種子也都是一樣的,但是步驟(8)的CAS操作會保證只有一個執行緒可以更新老的種子為新的,失敗的執行緒會通過迴圈從新獲取更新後的種子作為當前種子去計算老的種子,這就保證了隨機數的隨機性。
  • 程式碼(9)則使用固定演算法根據新的種子計算隨機數,並返回。

3.2 ThreadLocalRandom

Random類生成隨機數原理以及不足:每個Random例項裡面有一個原子性的種子變數用來記錄當前的種子的值,當要生成新的隨機數時候要根據當前種子計算新的種子並更新回原子變數。

多執行緒下使用單個Random例項生成隨機數時候,多個執行緒同時計算隨機數計算新的種子時候多個執行緒會競爭同一個原子變數的更新操作,由於原子變數的更新是CAS操作,同時只有一個執行緒會成功,那麼CAS操作失敗的大量執行緒進行自旋重試,而大量執行緒的自旋重試是會降低併發效能和消耗CPU資源的,為了解決這個問題,ThreadLocalRandom類應運而生。

public class RandomTest {

    public static void main(String[] args) {
        //(10)獲取一個隨機數生成器
        ThreadLocalRandom random =  ThreadLocalRandom.current();

        //(11)輸出10個在0-5(包含0,不包含5)之間的隨機數
        for (int i = 0; i < 10; ++i) {
            System.out.println(random.nextInt(5));
        }
    }
}

如上程式碼(10)呼叫ThreadLocalRandom.current()來獲取當前執行緒的隨機數生成器。下面來分析下ThreadLocalRandom的實現原理。

從名字看會讓我們聯想到ThreadLocal類。ThreadLocal通過讓每一個執行緒拷貝一份變數,每個執行緒對變數進行操作時候實際是操作自己本地記憶體裡面的拷貝,從而避免了對共享變數進行同步。實際上ThreadLocalRandom的實現也是這個原理。Random的缺點是多個執行緒會使用原子性種子變數,會導致對原子變數更新的競爭,這個原理可以通過下面圖來表達:

那麼如果每個執行緒維護自己的一個種子變數,每個執行緒生成隨機數時候根據自己本地記憶體中的老的種子計算新的種子,並使用新種子更新老的種子,然後根據新種子計算隨機數,就不會存在競爭問題,這會大大提高併發效能,如下圖ThreadLocalRandom原理可以使用下圖表達:

Thread類裡面有幾個變數:

    /** The current seed for a ThreadLocalRandom */
    @sun.misc.Contended("tlr")
    long threadLocalRandomSeed;

    /** Probe hash value; nonzero if threadLocalRandomSeed initialized */
    @sun.misc.Contended("tlr")
    int threadLocalRandomProbe;

思考問題:

  • 每個執行緒的初始種子怎麼生成的
  • 如果保障多個執行緒產生的種子不一樣

四、總結

本文是對拙作 java併發程式設計之美 一書中有關章節的提煉。本次分享首先講解了AtomicLong的內部實現,以及存在的缺點,然後講解了 LongAdder採用分而治之的策略通過使用多個原子變數減小單個原子變數競爭的併發度。然後簡單介紹了Random,和其缺點,最後介紹了ThreadLocalRandom借用ThreadLocal的思想解決了多執行緒對同一個原子變數競爭鎖帶來的效能損耗。其實JUC包中還有其他一些經典的元件,比如fork-join框架等。



本文作者:加多

閱讀原文

本文為雲棲社群原創內容,未經允許不得轉載。

相關文章