15-ThreadLocalRandom類剖析

黑夜中的小迷途發表於2021-10-15

ThraedLocalRandom類是JDK7在JUC包下新增的隨機數生成器,它彌補了Random類在多執行緒下的缺陷。

Random類及其缺陷

下面看一下java.util.Random的使用方法。

import java.util.Random;

public class RandomTest1 {
    public static void main(String[] args) {
        //建立一個預設種子的隨機數生成器
        Random random = new Random();
        //輸出10個[0,5)範圍的數
        for (int i = 0; i < 10; i++) {
            System.out.print(random.nextInt(5)+" ");
        }
    }
}
4 4 4 4 3 0 3 3 0 0 
Process finished with exit code 0

預設種子的隨機生成器使用的是預設的種子,這個種子是long型別的數字。

  public Random() {
        this(seedUniquifier() ^ System.nanoTime());
    }

有了預設種子後,如何生成隨機數呢?我們檢視一下nextInt()原始碼:

public int nextInt(int bound) {
    	//首先進行引數檢查,判斷輸入的範圍是否小於等於0
        if (bound <= 0)
            //如果小於等於0,則丟擲非法引數異常。
            throw new IllegalArgumentException(BadBound);
		//根據老的種子生成新的種子,
        int r = next(31);
    	//根據新的種子計算隨機數。
        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;
    }

根據老的種子生成新的種子,我們可以想象成這樣一個函式seed=f(seed),比如seed=f(seed)=a*seed+b;

根據新的種子計算生成數我們可以想像成g(seed,bound)=(int)(bound*(long)seed>>31)。在單執行緒下每次呼叫nextInt()方法都是根據老的的種子計算出新的種子,,這樣可以保證隨機數的產生是隨機性的。但是在多執行緒下多個執行緒可能都會拿到同一個老的種子去執行根據老的種子生成新的種子以計算新的種子。這會導致多個執行緒產生的額新種子是一樣的。由於根據新的種子計算隨機數這個演算法是不變的,所以在多執行緒下會產生相同的隨機數。這並不是我們想要的。為了保證在多執行緒下每一個執行緒獲取到的隨機數不一樣,當第一個執行緒的新種子計算出來之後,第二個執行緒就要丟棄掉自己的老種子,而是用第一個執行緒的新種子重新計算自己的新種子,以此類推,這樣才能保證多執行緒下產生的隨機數是隨機的。Random函式使用了一個原子變數到達了這個效果,在建立Random物件時初始化的種子就被儲存到種子原子變數裡面,下面是next()方法原始碼:

protected int next(int bits) {
        long oldseed, nextseed;
        AtomicLong seed = this.seed;
        do {
            //1
            oldseed = seed.get();
            //2
            nextseed = (oldseed * multiplier + addend) & mask;
            //3
        } while (!seed.compareAndSet(oldseed, nextseed));
    		//4
        return (int)(nextseed >>> (48 - bits));
    }

程式碼(1):獲取當前原子變數種子的值。

程式碼(2):根據當前種子值計算新的種子。

程式碼(3):使用CAS操作,它使用新的種子來更新舊的種子,CAS操作會保證只有一個執行緒可以更新老的種子的新的,失敗的執行緒會通過迴圈重新獲取更新後的種子作為當前種子去計算老的種子,這就保證了隨機數的隨機性。

程式碼(4):適用固定演算法根據新的種子計算隨機數。

總結:每一個Random例項裡面都有一個原子性的種子變數用來記錄當前的種子值,當要生成新的隨機數時需要根據當前種子計算出新的種子並更新返回原子變數,在多執行緒下使用單個Random例項生成隨機數時,當多個執行緒同時計算隨機數計算新的種子時,多個執行緒會競爭同一原子變數的更新操作,由於原子變數更新是CAS操作,同時只有一個執行緒會成功,所以大量執行緒進行自旋重試,這會降低併發效能,所以ThreadLocalRandom應運而生。

ThreadLocalRandom類

為了彌補高併發情況下Random的缺陷,在JUC包下新增了ThreadLocalRandom類,下面看一下如何使用它:

import java.util.concurrent.ThreadLocalRandom;

public class ThreadLocalRandomTest1 {
    public static void main(String[] args) {
        //(1)獲取一個隨機數生成器
        ThreadLocalRandom random=ThreadLocalRandom.current();
        //(2)輸出10個[0,5)範圍的數
        for (int i = 0; i < 5; i++) {
            System.out.print(random.nextInt(5)+" ");
        }
    }
}

執行結果

3 4 3 4 4 
Process finished with exit code 0

程式碼(10)呼叫ThreadLocalRandom.current()方法來獲取當前執行緒的隨機數生成器,下面來分析一下ThreadLocalRandom的實現原理:ThreadLocal通過讓每一個執行緒複製一份變數,使得在每個執行緒對變數進行執行緒操作時實際就是自己本地記憶體裡面的副本,從而避免了對共享變數進行同步。實際上ThreadLocalRandom實現的也是這個原理,Random的缺點就是多個執行緒會使用同一個原子性種子變數,從而導致對原理變數更新的競爭,如圖:

image

那麼如果每一個執行緒都維護一個種子變數,則每個執行緒生成隨機數都根據自己老的種子計算新的種子,並使用新的種子來更新老的種子,再根據新種子計算新的隨機數。就不會存在競爭問題了,這會大大提高併發性。

原始碼分析

首先檢視ThreadLocalRandom類結構:

image

從圖中可以看出ThreadLocalRandom類繼承了Random類,並重寫了nextInt()方法,在ThreadLocalRandom類中並沒有使用繼承自Random類的原子性種子變數,在ThreadLocalRandom中並沒有存放具體的種子,具體的種子存放在具體的呼叫執行緒的ThreadLocalRandom例項裡面,ThreadLocalRandom類似於ThreadLocal類,是一個工具類,當執行緒呼叫ThreadLocalRandom.current()方法的時候,ThreadLocalRandom負責初始化呼叫ThreadLocalRandomSeed變數,也就是初始化種子。

當呼叫 ThreadLocalrandon的 nextInt方法時,實際上是獲取當前執行緒的threadLocalRandom Seed變數作為當前種子來計算新的種子,然後更新新的種子到當前執行緒的 threadLocalRandom Seed變數,而後再根據新種子並使用具體演算法計算隨機數。這裡需要注意的是, threadLocalRandom Seed變數就是 Thread類裡面的一個普通long變數,它並不是原子性變數。

其中seeder和probeGenerator是兩個原子性變數,在初始化呼叫執行緒的種子和探針變數時會引用它們,每個執行緒只會使用一次。

另外,變數 instance是 ThreadLocalRandom的一個例項,該變數是 static的。當多執行緒通過 ThreadLocalRandom的 current方法獲取 ThreadLocalrandom的例項時,其實獲取的是同一個例項。但是由於具體的種子是存放線上程裡面的,所以在 Threadlocalrandom的例項裡面只包含與執行緒無關的通用演算法,所以它是執行緒安全的。
下面看看 ThreadLocalrandom的主要程式碼的實現邏輯。

  1. Unsafe機制

    private static final sun, misc. UnsafeUNSAFE
    private static final long SEED
    private static final long ProBE;
    private static final long secondArY;
    
    static{
        try{
            //獲取 unsafe例項
    		UNSAFE sun. misc. Unsafe. getUnsafe();
    		Class<?> tk= Thread class;
    		//獲取 Thread類裡面 threadloca1 RandomSeed變數在 Thread例項裡面的偏移量
            SEED= UNSAFE. objectFieldoffset
            (tk getDeclaredField( threadlocalRandomSeed ));
            //獲取 Thread類裡面 threadlocalrandomProbe變數在 Thread例項裡面的偏移量
            PROBE= UNSAFE. objectFieldoffset
            (tk getDeclaredField("threadLocalRandomProbe" ));
            //獲取 Thread類裡面 threadLocalRandomSecondarySeed變數在 Thread例項裡面的偏移
            量,這個值在後面講解 LongAdder時會用到
            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 (){
        //(1)
        if (UNSAFE getInt(Thread currentThread(), PROBE)==0)
        //(2)
        localInit();
        //(3)
        return instance;
    }
        
    static final void localInit{
        int p= probe Generator. addAndGet( PROBE INCremENT );
        int probe =(p==0)? 1: p;//skip 0
        long seed =mix64(seeder. getAndAdd (SEEDER INCREMENT));
        Thread t Thread currentThread();
        UNSAFE pulOng(t, SEED, seed);
        UNSAFE. putInt(t, PROBE, probe);
    }
    

    程式碼(1):如果當前執行緒threadLocalRandomProbe的變數值為0(預設為0),則說明當前執行緒是第一次呼叫ThreadLocalRandom的current()方法,那麼就需要呼叫 locallnit方法計算當前執行緒的初始化種子變數。這裡為了延遲初始化,在不需要使用隨機數功能時就不初始化 Thread類中的種子變數,這是一種優化。
    程式碼(2):首先根據 probeGenerator計算當前執行緒中 threadLocalRandom Probe的初始化值,然後根據 seeder計算當前執行緒的初始化種子,而後把這兩個變數設定到當前執行緒。
    程式碼(3):返回 ThreadLocalRandom的例項。需要注意的是,這個方法是靜態方法,多個執行緒返回的是同一個 ThreadLocalRandom例項。

  3. int nextInt(int bound)方法。

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

       public int nextInt(int bound) {
           	//引數校驗
            if (bound <= 0)
                throw new IllegalArgumentException(BadBound);
           	//根據當前執行緒中的種子計算新種子
            int r = mix32(nextSeed());
           	//根據新種子和bound計算隨機數
            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;
        }
    
  4. 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. geeLong(t,SEED)獲取當前執行緒中threadLocalRandom Seed變數的值,然後在種子的基礎上累加 GAMMA值作為新種子,而後使用 UNSAFE的 pulOng方法把新種子放入當前執行緒的 threadLocalRandom Seed變數中。

總結

該部分主要講解了 Random的實現原理以及 Random在多執行緒下需要競爭種子原子變數
更新操作的缺點,從而引出 ThreadLocalRandom類。 Threadlocalrandom使用 Threadlocal
的原理,讓每個執行緒都持有一個本地的種子變數,該種子變數只有在使用隨機數時才會被
初始化。在多執行緒下計算新種子時是根據自己執行緒內維護的種子變數進行更新,從而避免
了競爭。

相關文章