1. 程式人生 > 其它 >JUC中ThreadLocalRandom原始碼分析

JUC中ThreadLocalRandom原始碼分析

ThreadLocalRandom 是 JDK7 在 JUC 包下新增的隨機數生成器,它解決了 Random 在多執行緒下多個執行緒競爭內部唯一的原子性種子變數而導致大量執行緒自旋重試的不足。
需要注意的是 Random 本身是執行緒安全的。同時 Random 例項不是安全可靠的加密,可以使用 java.security.SecureRandom 來提供一個可靠的加密。

1. 隨機數演算法介紹

常用的隨機數演算法有兩種:同餘法(Congruential method)和梅森旋轉演算法(Mersenne twister)。Random 類中用的就是同餘法中的一種 - 線性同餘法(見Donald Kunth《計算機程式設計的藝術》第二卷,章節3.2.1)。

在程式中為了使表示式的結果小於某個值,常常採用取餘的操作,結果是同一個除數的餘數,這種方法叫同餘法(Congruential method)。

線性同餘法是一個很古老的隨機數生成演算法,它的數學形式如下:

Xn+1= (a * Xn+ c) % m

其中,m > 0, 0 < a < m, 0 < c < m

https://blog.csdn.net/lihui126/article/details/46236109

2. Random 原始碼分析

JDK 中的 Random 類生成的是偽隨機數,使用的是 48-bit 的種子,然後呼叫線性同餘方程,程式碼很簡潔。

2.1 資料結構

private static final long multiplier = 0x5DEECE66DL;    // 相當於上面表示式中的 a
private static final long mask = (1L << 48) - 1;        // 相當於上面表示式中的 m
private static final long addend = 0xBL;                // 相當於上面表示式中的 c

// seed 生成的隨機數種子
private final AtomicLong seed;

2.2 建構函式

// ^ 讓 seed 更加隨機
public Random() {
    
this(seedUniquifier() ^ System.nanoTime()); } public Random(long seed) { if (getClass() == Random.class) // initialScramble 初始化的隨機數 this.seed = new AtomicLong(initialScramble(seed)); else { this.seed = new AtomicLong(); // 子類重寫 setSeed setSeed(seed); } } private static final AtomicLong seedUniquifier = new AtomicLong(8682522807148012L); private static long seedUniquifier() { for (;;) { long current = seedUniquifier.get(); long next = current * 181783497276652981L; if (seedUniquifier.compareAndSet(current, next)) return next; } } // 初始化的隨機數 private static long initialScramble(long seed) { return (seed ^ multiplier) & mask; }

建構函式初始化了隨機數種子 seed,之後的隨機數都是在這個基礎上進行計算的。 如果傳入的 seed 值一樣,那麼生成的隨機數也就是一樣的了。

@Test
public void test() {
    long seed = 343L;
    Random random1 = new Random(seed);
    Random random2 = new Random(seed);

    Assert.assertEquals(random1.nextInt(), random2.nextInt());
    Assert.assertEquals(random1.nextInt(), random2.nextInt());
    Assert.assertEquals(random1.nextInt(), random2.nextInt());
}

2.3 生成隨機數

public int nextInt() {
    return next(32);
}
public int nextInt(int bound) {
    if (bound <= 0)
        throw new IllegalArgumentException(BadBound);
    // 1. 生成隨機數
    int r = next(31);
    int m = bound - 1;
    // 2. 生成的隨機數不能超過 bound。   (n&-n)==n 也可以判斷2^n
    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;
}

protected int next(int bits) {
    long oldseed, nextseed;
    AtomicLong seed = this.seed;
    do {
        oldseed = seed.get();
        // 就這麼一句程式碼,對比上面的隨機數演算法
        nextseed = (oldseed * multiplier + addend) & mask;  
    } while (!seed.compareAndSet(oldseed, nextseed));
    return (int)(nextseed >>> (48 - bits));
}

可以看到上面程式碼可知新的隨機數的生成需要兩個步驟:

(1) 首先需要根據老的種子生成新的種子。
(2) 然後根據新的種子來計算新的隨機數。

3. ThreadLocalRandom 原始碼分析

為了解決多執行緒高併發下 Random 的缺陷,JUC 包下新增了 ThreadLocalRandom 類。更多參考併發包中ThreadLocalRandom類原理剖析

3.1 ThreadLocalRandom 原理

@Test
public void testThreadLocalRandom() {
    ThreadLocalRandom random = ThreadLocalRandom.current();
    System.out.println(random.nextInt());
}

從名字看會讓我們聯想到基礎篇講解的 ThreadLocal,ThreadLocal 的出現就是為了解決多執行緒訪問一個變數時候需要進行同步的問題,讓每一個執行緒拷貝一份變數,每個執行緒對變數進行操作時候實際是操作自己本地記憶體裡面的拷貝,從而避免了對共享變數進行同步

實際上 ThreadLocalRandom 的實現也是這個原理,Random 的缺點是多個執行緒會使用同一個原子性種子變數,會導致對原子變數更新的競爭。那麼如果每個執行緒維護自己的一個種子變數,每個執行緒生成隨機數時候根據自己老的種子計算新的種子,並使用新種子更新老的種子,然後根據新種子計算隨機數,就不會存在競爭問題。這會大大提高併發效能,如下圖 ThreadLocalRandom 原理:

3.2 資料結構

從 ThreadLocalRandom 類圖中可以看到和 Random 儲存一份 seed 不同,ThreadLocalRandom 的種子變數儲存在 Thread.threadLocalRandomSeed 變數中,通過 Unsafe 操作這個變數。關於 threadLocalRandomSeed、threadLocalRandomProbe、threadLocalRandomSecondarySeed 這三個變數,Thread 類有相關的註釋:

/** The current seed for a ThreadLocalRandom */
// 1. 和 Random 中的 seed 類似
long threadLocalRandomSeed;

/** Probe hash value; nonzero if threadLocalRandomSeed initialized */
// 2. 在 CurrentHashMap 中有使用。probe 是探測的意思,
int threadLocalRandomProbe;

/** Secondary seed isolated from public ThreadLocalRandom sequence */
int threadLocalRandomSecondarySeed;

需要注意的是這三個值都不能為 0,因為 0 在 ThreadLocalRandom 中有特殊的含義,表示還未初始化,呼叫 localInit() 進行初始化

3.3 建構函式

boolean initialized;
private ThreadLocalRandom() {
    initialized = true; // false during super() call
}
public static ThreadLocalRandom current() {
    if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
        localInit();
    return instance;
}

ThreadLocalRandom 建構函式為私有的,只能通過 current 方法構建,如果 PROBE 還是預設值 0 表示未初始化,呼叫 localInit 進行初始化。

3.4 生成隨機數 nextInt

// Random 一樣也有兩步:一是根據老的種子生成新的種子;
//                     二是根據新的種子來計算新的隨機數
public int nextInt() {
    return mix32(nextSeed());
}

public int nextInt(int bound) {
    if (bound <= 0)
        throw new IllegalArgumentException(BadBound);
    int r = mix32(nextSeed());
    // 1. 生成隨機數
    int m = bound - 1;
    // 2. 生成的隨機數不能超過 bound
    // 2.1 bound 是 z^n 則直接取餘
    if ((bound & m) == 0) // power of two
        r &= m;
    // 2.2取 [0, bound] 之間的數
    else { // reject over-represented candidates
        for (int u = r >>> 1; u + m - (r = u % bound) < 0; u = mix32(nextSeed()) >>> 1)
            ;
    }
    return r;
}

ThreadLocalRandom 和 Random 一樣也有兩步:

(1) 根據老的種子生成新的種子;
(2) 根據新的種子來計算新的隨機數。

nextInt(int bound) 和 nextInt 的思路是一樣的,先呼叫 mix32(nextSeed()) 函式生成隨機數(int型別的範圍),再對引數 n 進行判斷,如果 n 恰好為 2 的方冪,那麼直接移位就可以得到想要的結果;如果不是 2 的方冪,那麼就關於 n 取餘,最終使結果在[0,n)範圍內。另外,for 迴圈語句的目的應該是防止結果為負數。

當bound為2n時, bound與生成的隨機數相乘, 相當於取隨機數的前log2bound
其它情況時, 將int的取值範圍231−1以bound為區間範圍劃分為n組, 最後一個區間的數不夠bound個, 如果生成的隨機數是從這個區間內生成的, 則難以保證隨機性, 故需要重新生成.

// 生成新的種子,儲存在 Thread.threadLocalRandomSeed 中。 GAMMA=0x9e3779b97f4a7c15L
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;
}
// 根據新種子生成隨機數,隨機數演算法和 Random 一樣了
private static int mix32(long z) {
    z = (z ^ (z >>> 33)) * 0xff51afd7ed558ccdL;
    return (int)(((z ^ (z >>> 33)) * 0xc4ceb9fe1a85ec53L) >>> 32);
}

3.5 其它方法

(1) getProbe

getProbe 用法參考 ConcurrentHashMap#fullAddCount 方法。

static final int getProbe() {
    return UNSAFE.getInt(Thread.currentThread(), PROBE);
}

static final int advanceProbe(int probe) {
    probe ^= probe << 13;   // 異或位運算。 xorshift
    probe ^= probe >>> 17;
    probe ^= probe << 5;
    UNSAFE.putInt(Thread.currentThread(), PROBE, probe);
    return probe;
}

(2) nextSecondarySeed

static final int nextSecondarySeed() {
    int r;
    Thread t = Thread.currentThread();
    if ((r = UNSAFE.getInt(t, SECONDARY)) != 0) {
        r ^= r << 13;   // xorshift
        r ^= r >>> 17;
        r ^= r << 5;
    }
    else {
        localInit();
        if ((r = (int)UNSAFE.getLong(t, SEED)) == 0)
            r = 1; // avoid zero
    }
    UNSAFE.putInt(t, SECONDARY, r);
    return r;
}

參考:

  1. 併發包中ThreadLocalRandom類原理剖析
  2. 《ThreadLocalRandom和Random效能測試》:http://www.importnew.com/12460.html
  3. 《Java中的random函式是如何實現的》:https://my.oschina.net/hosee/blog/600392
  4. 《解密隨機數生成器》:https://blog.csdn.net/lihui126/article/details/46236109
  5. 多執行緒下ThreadLocalRandom用法 - 簡書 (jianshu.com)