15-ThreadLocalRandom類剖析


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
的原理,讓每個線程都持有一個本地的種子變量,該種子變量只有在使用隨機數時才會被
初始化。在多線程下計算新種子時是根據自己線程內維護的種子變量進行更新,從而避免
了競爭。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM