Java JUC ThreadLocalRandom類解析

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

ThreadLocalRandom 類解析

前言

ThreadLocalRandom 類是 JDK7 在JUC包下新增的隨機數生成器,它主要解決了 Random 類在多執行緒下的不足。

本文主要講解為什麼需要 ThreadLocalRandom 類,以及該類的實現原理。

Random 類及其侷限性

首先我們先了解一下 Random 類。在 JDK7 以前到現在,java.util.Random 類都是使用較為廣泛的隨機數生成工具類,而且 java.lang.Math 的隨機數生成也是使用的 java.util.Random 類的例項,下面先看看如何使用 Random 類。

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.print(random.nextInt(5)); //3421123432
        }
}

隨機數的生成需要一個預設種子,這個種子其實是一個long型別的數字,可以通過在建立 Random 類物件時通過建構函式指定,如果不指定則在預設建構函式內部生成一個預設的值。

種子數只是隨機演算法的起始數字,和生成的隨機數字的區間無關。
public Random() {
        this(seedUniquifier() ^ System.nanoTime());
}
public Random(long seed) {
        if (getClass() == Random.class)
            this.seed = new AtomicLong(initialScramble(seed));
        else {
            // subclass might have overriden setSeed
            this.seed = new AtomicLong();
            setSeed(seed);
        }
}

在有了預設種子之後,Random 是如何生成隨機數的呢?我們看一下 nextInt()方法。

public int nextInt(int bound) {
              //3. 引數檢查
        if (bound <= 0)
            throw new IllegalArgumentException(BadBound);
                //4. 根據老的種子生成新的種子
        int r = next(31);
              //5. 根據新的種子計算隨機數
        int m = bound - 1;
        if ((bound & m) == 0)  // i.e., bound is a power of 2
            r = (int)((bound * (long)r) >> 31);
        else {
            for (int u = r;
                 u - (r = u % bound) + m < 0;
                 u = next(31))
                ;
        }
        return r;
}

由此可見,新的隨機數的生成需要兩個步驟:

  • 根據老的種子生成新的種子
  • 然後根據新的種子來計算新的隨機數

其中步驟4可以抽象為seed = f(seed),比如seed = f(seed) = a*seed+b

步驟5可以抽象為g(seed,bound),比如g(seed,bound) = (int) ((bound * (long) seed) >> 31)

在單執行緒情況下每次呼叫nextInt()都是根據老的種子計算出新的種子,這是可以保證隨機數產生的隨機性。但在多執行緒下多個執行緒可能拿同一個老的種子去執行步驟4以計算新的種子,這會導致多個執行緒的新種子是一樣的,並且由於步驟5的演算法是固定的,所以會導致多個執行緒產生相同的隨機值

所以步驟4要保證原子性,也就是說當多個執行緒根據同一個老種子計算新種子時,第一個執行緒的新種子被計算出來後,第二個執行緒要丟棄自己老的種子,而使用第一個執行緒的新種子來計算自己的新種子,依此類推。

? 在 Random 中使用了原子變數 AtomicLong來達到這個效果,在建立 Random 物件時初始化的種子就被儲存到了種子原子變數裡面

接下來看一下 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. 使用CAS操作,它使用新的種子去更新老的種子,失敗的執行緒會通過迴圈重新獲取更新後的種子作為當前種子去計算老的種子
        } while (!seed.compareAndSet(oldseed, nextseed));
        return (int)(nextseed >>> (48 - bits));
}

總結:在多執行緒下雖然同一時刻只有一個執行緒會成功,但是會造成大量執行緒進行自旋操作,這樣會降低併發效能,所以 ThreadLocalRandom 運用而生。

ThreadLocalRandom

為了彌補 Random 在多執行緒下的效率問題,在 JUC 包中增加了 ThreadLocalRandom 類,下面先演示如何使用 ThreadLocalRandom 類:

public static void main(String[] args) {
        //1. 獲取一個隨機數生成器
        ThreadLocalRandom random = ThreadLocalRandom.current();
        //2. 輸出10個在0-5(包含0,不包含5)之間的隨機數
        for (int i = 0; i < 10; ++i) {
            System.out.print(random.nextInt(5));
        }

}

其中第一步呼叫ThreadLocalRandom.current()來獲取當前執行緒的隨機數。

實際上 ThreadLocalRandom 的實現和 ThreadLocal 差不多,都是通過讓每一個執行緒複製一份變數,從而讓每個執行緒實際操作的都是本地記憶體中的副本,避免了對共享變數的同步。

Random 的缺點是多個執行緒會使用同一個原子性種子變數,從而導致對原子變數更新的競爭。

而在 ThreadLocalRandom 中,每個執行緒都維護一個種子變數,在每個執行緒生成隨機數的時候都根據當前執行緒中舊的種子去計算新的種子,並使用新的種子更新老的種子,再根據新的種子去計算隨機數,這樣就不會存在競爭問題了。

ThreadLocalRandom

原始碼解析

首先我們先看一下 ThreadLocalRandom 的類圖結構。

類圖結構

從圖中可以看到 ThreadLocalRandom 繼承了 Random 類並重寫了nextInt()方法,在 ThreadLocalRandom 類中並沒有使用 Random 的原子性種子變數。

在 ThreadLocalRandom 中並沒有存放具體的種子,具體的種子都放在具體的呼叫執行緒的 ThreadLocalRandomSeed 中變數中。

ThreadLocalRandom 類似於 ThreadLocal 類,是個工具類。當執行緒呼叫 ThreadLocalRandom 的 current()方法時,ThreadLocalRandom 負責初始化呼叫執行緒的 threadLocalRandomSeed 變數,進行初始化種子。

在呼叫 ThreadLocalRandom 的nextInt()方法時,實際就是獲取當前執行緒的 ThreadLocalRandomSeed 變數作為當前種子去計算新種子,然後更新新的種子到 ThreadLocalRandomSeed 中,隨後再根據新的種子去計算隨機數。

需要注意的是:threadLocalRandomSeed 變數就是 Thread 類中的一個普通 long 型別的變數,不是原子型別變數。

@sun.misc.Contended("tlr")
long threadLocalRandomSeed;

因為這個變數是執行緒級別的,根本不需要使用原子型別變數。

ThreadLocalRandom 中的 seeder 和 probeGenerator 是兩個原子性變數,將 probeGenerator 和 seeder 宣告為原子變數的目的是為了在多執行緒情況下,賦予它們各自不同的種子初始值,這樣就不會導致每個執行緒產生的隨機數序列都是一樣的,而且 probeGenerator 和 seeder 只會在初始化在初始化呼叫執行緒的種子和探針變數(用於分散計算陣列索引下標)時候用到,每個執行緒只會使用一次。

另外變數 instance 是 ThreadLocalRandom 的一個例項,該變數是 static,多個執行緒使用的例項是同一個,但是由於具體的種子存在線上程裡面的,所以在 ThreadlocalRandom 的例項裡面只包含執行緒無關的的通用演算法,因此它是執行緒安全的

下面來看一下 ThreadLocalRandom 類的主要程式碼邏輯:

1.Unsafe 機制

    // Unsafe mechanics
    private static final sun.misc.Unsafe UNSAFE;
    private static final long SEED;
    private static final long PROBE;
    private static final long SECONDARY;
    static {
        try {
              //獲取例項
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class<?> tk = Thread.class;
            //獲取Thread類中的threadLocalRandomSeed變數在Thread例項裡面的偏移量
            SEED = UNSAFE.objectFieldOffset
                (tk.getDeclaredField("threadLocalRandomSeed"));
            //獲取Thread類中的threadLocalRandomProbe變數在Thread例項裡面的偏移量
            PROBE = UNSAFE.objectFieldOffset
                (tk.getDeclaredField("threadLocalRandomProbe"));
            //獲取Thread類中的threadLocalRandomSecondarySeed變數在Thread例項裡面的偏移量
            SECONDARY = UNSAFE.objectFieldOffset
                (tk.getDeclaredField("threadLocalRandomSecondarySeed"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }

2.ThreadLocalRandom current()方法

該方法獲取 ThreadLocalRandom 例項,並初始化呼叫執行緒中的 threadLocalRandomSeed、threadLocalRandomProbe 變數。

static final ThreadLocalRandom instance = new ThreadLocalRandom();
public static ThreadLocalRandom current() {
        if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
            localInit();
        return instance;
}
static final void localInit() {
        int p = probeGenerator.addAndGet(PROBE_INCREMENT);
        int probe = (p == 0) ? 1 : p; // skip 0
        long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));
        Thread t = Thread.currentThread();
        UNSAFE.putLong(t, SEED, seed);
        UNSAFE.putInt(t, PROBE, probe);
}

在上面程式碼中,如果當前執行緒中的 threadLocalRandomProbe 的變數值為 0(預設情況下執行緒的這個變數為 0),則說明當前執行緒是第一次呼叫current()方法,那麼需要呼叫localInit()方法計算當前執行緒的初始化種子變數。

這裡為了延遲初始化,在不需要隨機數功能的時候就不初始化 Thread 類中的種子變數。

localInit()中,首先根據 probeGenerator 計算當前執行緒中的 threadLocalRandomProbe 初始化值,然後根據 seeder 計算當前執行緒的初始化種子,然後把這兩個變數設定到當前執行緒中。

在 current 最後返回 ThreadLocalRandom 的例項。需要注意的是,這個方法是靜態方法,多個執行緒返回的是同一個 ThreadLocalRandom 例項

3.int nextInt(int bound)方法

計算當前執行緒的下一個隨機數。

public int nextInt(int bound) {
        //引數校驗
        if (bound <= 0)
            throw new IllegalArgumentException(BadBound);
        //根據當前執行緒中的種子計算新種子
        int r = mix32(nextSeed());
        //根據新種子計算隨機數
        int m = bound - 1;
        if ((bound & m) == 0) // power of two
            r &= m;
        else { // reject over-represented candidates
            for (int u = r >>> 1;
                 u + m - (r = u % bound) < 0;
                 u = mix32(nextSeed()) >>> 1)
                ;
        }
        return r;
}

上面邏輯和 Random 類似,重點是nextSeed()方法,該方法主要就是為了獲取並更新各自的種子變數並生成隨機數。

final long nextSeed() {
        Thread t; long r; // read and update per-thread seed
        UNSAFE.putLong(t = Thread.currentThread(), SEED,
                       r = UNSAFE.getLong(t, SEED) + GAMMA);
        return r;
}

在這段程式碼中,首先使用變數r = UNSAFE.getLong(t,SEED)後去獲取當前執行緒中 threadLocalRandomSeed 變數的值,然後在種子的基礎上累加GAMMA值作為新的種子,之後使用UNSAFE的putLong方法把新的種子放入當前執行緒 threadLocalRandomSeed 變數中。

4.long initialSeed()方法

private static final AtomicLong seeder = new AtomicLong(initialSeed());

    private static long initialSeed() {
        String sec = VM.getSavedProperty("java.util.secureRandomSeed");
        if (Boolean.parseBoolean(sec)) {
            byte[] seedBytes = java.security.SecureRandom.getSeed(8);
            long s = (long)(seedBytes[0]) & 0xffL;
            for (int i = 1; i < 8; ++i)
                s = (s << 8) | ((long)(seedBytes[i]) & 0xffL);
            return s;
        }
        return (mix64(System.currentTimeMillis()) ^
                mix64(System.nanoTime()));
}

在初始化種子變數的初始值對應的原子變數 seeder 時,呼叫了initialSeed()方法,首先判斷java.util.secureRandomSeed的系統屬性值是否為 true 來判斷是否使用安全性高的種子,如果為 true 則使用java.security.SecureRandom.getSeed(8)獲取高安全性種子,如果為 false 則根據當前時間戳來獲取初始化種子,也就是說使用安全性高的種子是無法被預測的,而 Random、ThreadLocalRandom 產生的被稱為“偽隨機數”,因為是可被預測的。

總結

ThreadLocalRandom 使用 ThreadLocal 的原理,讓每個執行緒都持有一個本地的種子變數,該種子變數只有在使用隨機數時才會被初始化。在多執行緒下計算新種子時是根據自己執行緒內維護的種子變數進行更新,從而避免了競爭。

相關文章