Random和ThreadLocalRandom原理分析

Random和ThreadLocalRandom都是用於生成隨機數的。用法比較簡單,這裏直接看代碼分析原理。

Random

Random隨機數的實現幾乎都是依賴於內部的next方法,都是在next的基礎上進行額外的計算,這裏直接看next實現:

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

Random獲取隨機數分兩步:

  1. 獲得老的種子數據
  2. 通過老的種子計算新的種子

最後,用老的種子替換新的種子,這個我們都能理解,但是這裏使用的是CAS進行替換,這個是爲什麼呢?

如果不用CAS,再看上面代碼,如果是多個線程同時並行訪問,會出現:多個線程同時獲得了同一個老的種子,然後根據老的種子計算出同樣的新的種子,這就導致了多個線程獲得的隨機數相同。

所以,介於這種情況,使用CAS來進行控制,使得在多線程的情況下,保證每個線程獲取的隨機數是不同的。如果CAS失敗,說明老的種子已經被使用,所以會繼續循環重新拿到最新的老的種子再去計算新的種子,直到成功並返回。

Random確實很好的解決了多線程下的問題,但是這又帶來了新的問題:在線程競爭激烈的情況下,多個線程通過CAS競爭一個seed,導致大多數線程都在自旋,嚴重降低了多線程下獲取隨機數的效率,所以ThreadLocalRandom就是爲了解決這個問題。

ThreadLocalRandom

初始化

ThreadLocalRandom的原理其實也很簡單。既然Random多線程共享seed,競爭激烈導致效率低,那爲什麼不每個線程都擁有自己的seed呢,每個線程每次通過自己的當前seed獲取新的seed呢?是的,ThreadLocalRandom就是這樣做的。

那每個線程如果都擁有自己的seed,首先每個線程的seed保存在哪兒呢?不用多想,當前是當前線程對象了(ThreadLocal在線程本地也有自己的ThreadLocalMap,原理是類似的)。
爲了實現ThreadLocalRandom,在Thread中有如下字段:

 /** The current seed for a ThreadLocalRandom */
 //用於產生隨機數的種子
 @sun.misc.Contended("tlr")
 long threadLocalRandomSeed;
 /** Probe hash value; nonzero if threadLocalRandomSeed initialized */
 //如果threadLocalRandomSeed被初始化則threadLocalRandomProbe不爲0
 @sun.misc.Contended("tlr")
 int threadLocalRandomProbe;
 /** Secondary seed isolated from public ThreadLocalRandom sequence */
 @sun.misc.Contended("tlr")
 //用來生成threadLocalRandomSeed的輔助種子
 int threadLocalRandomSecondarySeed;

從Thread中字段的定義來看,threadLocalRandomSeed是沒有初始值得。可想而知:只有用到ThreadLocalRandom的時候,才需要threadLocalRandomSeed,需要的時候再進行初始化,避免不必要的內存消耗。

ThreadLocalRandom是在第一次調用current的時候,對當前threadLocalRandomSeed進行的初始化的,並且ThreadLocalRandom是單例模式:

public static ThreadLocalRandom current() {
    if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
        localInit();
    return instance;
}

對上面代碼中的UNSAFE.getInt(Thread.currentThread(), PROBE)不理解的,這裏簡單講一下unsafe相關的知識。先看這句代碼,找到UNSAFE和PROBE到底是什麼,在ThreadLocalRandom中有如下定義:

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;
        SEED = UNSAFE.objectFieldOffset
            (tk.getDeclaredField("threadLocalRandomSeed"));
        PROBE = UNSAFE.objectFieldOffset
            (tk.getDeclaredField("threadLocalRandomProbe"));
        SECONDARY = UNSAFE.objectFieldOffset
            (tk.getDeclaredField("threadLocalRandomSecondarySeed"));
    } catch (Exception e) {
        throw new Error(e);
    }
}

可以看到,UNSAFE的類型是sun.misc.Unsafe,而PROBE,通過UNSAFE.objectFieldOffset獲取的。其實,在unsafe中,定義了很多的直接內存訪問操作,操作更高效,但是也存在一定的風險。unsafe可以通過getDeclaredField獲得某個對象中某個字段的偏移量,然後可以直接通過字段偏移量獲取某個對象的字段值,也可以通過字段偏移量給對象的字段賦值。當然,unsafe的功能不止於此,對於unsafe更詳細的介紹可以直接看 Unsafe應用解析
這裏就是,先通過static初始化拿到線程中threadLocalRandomSeed等變量的偏移量,然後在用到這些變量的時候,通過初始化得到的偏移量,使用get和put取值和賦值操作。

好,現在繼續回過頭看ThreadLocalRandom的創建:

public static ThreadLocalRandom current() {
	//拿到當前線程對象中threadLocalRandomProbe的值,如果爲0(默認值就是0),說明初始種子沒有初始化,所以需要先進行初始化。
    if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
        localInit();
   	//instance是static,ThreadLocalRandom 是單例模式,因爲seed保存在當前的線程中,所以沒必要每次都創建一個ThreadLocalRandom 
    return instance;
}
static final void localInit() {
	//重新計算threadLocalRandomProbe,因爲已經初始化了,所以threadLocalRandomProbe一定不能爲0了。
    int p = probeGenerator.addAndGet(PROBE_INCREMENT);
    int probe = (p == 0) ? 1 : p; // skip 0
    //計算初始種子,這裏關鍵是seeder。
    //seeder是靜態變量,在類加載的時候會初始化一個最初始的值。
    //這裏在計算seed的值得同時,每來一個線程進行初始化,這裏都會爲seeder的值加SEEDER_INCREMENT,保證下一個線程的seeder和前一個的不一樣。
    //所以當所有線程第一次同時來競爭初始化的時候,會存在競爭,初始化之後,就不會再競爭了。
    long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));
    Thread t = Thread.currentThread();
    //將計算後的初始化值通過unsafe賦值給當前線程的變量
    UNSAFE.putLong(t, SEED, seed);
    UNSAFE.putInt(t, PROBE, probe);
}

初始化種子,關鍵是seeder,seeder定義與初始化如下:

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

private static long initialSeed() {
	//這兒可以配置啓動參數來選擇通過哪種方式進行初始化seeder。
    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是靜態變量,所以在類加載的時候,就初始化好了。因爲每個線程調用localInit都會改變seeder的值,也就保證了每個線程的seeder是不一樣的。

nextSeed

ThreadLocalRandom獲取隨機數,基本都是通過nextSeed實現的,nextSeed代碼如下,和Random原理相同,分爲三步:

final long nextSeed() {
    Thread t; long r; // read and update per-thread seed
    //1、獲取老的種子
    //2、計算新的種子
    //3、將新的種子覆蓋老的種子
    UNSAFE.putLong(t = Thread.currentThread(), SEED,
                   r = UNSAFE.getLong(t, SEED) + GAMMA);
    return r;
}
  1. 獲取老的種子 : UNSAFE.getLong(t, SEED)
  2. 計算新的種子:r = UNSAFE.getLong(t, SEED) + GAMMA
  3. 將新的種子覆蓋老的種子 UNSAFE.putLong(t = Thread.currentThread(), SEED, r = UNSAFE.getLong(t, SEED) + GAMMA);

和Random的區別就是:種子保存在當前線程對象中。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章