JAVA中生成隨機數Random VS ThreadLocalRandom效能比較

JAVA旭陽發表於2022-12-14

前言

大家專案中如果有生成隨機數的需求,我想大多都會選擇使用Random來實現,它內部使用了CAS來實現。 實際上,JDK1.7之後,提供了另外一個生成隨機數的類ThreadLocalRandom,那麼他們二者之間的效能是怎麼樣的呢?

Random的使用

Random類是JDK提供的生成隨機數的類, 這個類不是隨機的,而是偽隨機的。什麼是偽隨機呢? 偽隨機是指生成的隨機數是有一定規律的,這個規律出現的週期因偽隨機演算法的優劣而異。 一般來說,週期比較長,但可以預見。 我們可以透過以下程式碼簡單地使用 Random:

Random中有很多方法。 這裡我們就分析比較常見的nextInt()nextInt(int bound)方法。

  • nextInt()會計算int範圍內的隨機數,
  • nextInt(int bound)會計算[0,bound) 之間的隨機數,左閉右開。

實現原理

Random類的建構函式如下圖所示:

  • 可以看到在構造方法中,根據當前時間seed生成了一個AtomicLong型別的seed
public int nextInt() {
    return next(32);
}
  • 這裡面直接呼叫了next()方法,傳入了32,這裡的32是指Int的位數。
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));
}
  • 這裡會根據seed的當前值,透過一定的規則(偽隨機)計算出下一個seed,然後進行CAS。 如果CAS失敗,繼續迴圈上述操作。 最後根據我們需要的位數返回。

小結:可以看出在next(int bits)方法中,對AtomicLong進行了CAS操作,如果失敗則迴圈重試。 很多人一看到CAS,因為不需要加鎖,第一時間就想到了高效能、高併發。 但是在這裡,卻成為了我們多執行緒併發效能的瓶頸。 可以想象,當我們有多個執行緒執行CAS時,只有一個執行緒一定會失敗,其他的會繼續迴圈執行CAS操作。 當併發執行緒較多時,效能就會下降。

ThreadLocalRandom的使用

JDK1.7之後,提供了一個新類ThreadLocalRandom來替代Random

實現原理

我們先來看下current()方法。

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);
}
  • 如果沒有初始化,先進行初始化,這裡我們的seed不再是全域性變數了。 我們的執行緒中有三個變數:
/** The current seed for a ThreadLocalRandom */
@sun.misc.Contended("tlr")
long threadLocalRandomSeed;

/** Probe hash value; nonzero if threadLocalRandomSeed initialized */
@sun.misc.Contended("tlr")
int threadLocalRandomProbe;

/** Secondary seed isolated from public ThreadLocalRandom sequence */
@sun.misc.Contended("tlr")
int threadLocalRandomSecondarySeed;
  • threadLocalRandomSeed:這是我們用來控制隨機數的種子。
  • threadLocalRandomProbe:這個就是ThreadLocalRandom,用來控制初始化。
  • threadLocalRandomSecondarySeed:這是二級種子。

關鍵程式碼如下:

UNSAFE.putLong(t = Thread.currentThread(), SEED,r=UNSAFE.getLong(t, SEED) + GAMMA);

可以看出,由於每個執行緒都維護自己的seed,所以此時不需要CAS,直接進行put。 這裡透過執行緒間的隔離來減少併發衝突,所以ThreadLocalRandom的效能非常高。

效能對比

透過基準工具JMH測試:

@BenchmarkMode({Mode.AverageTime})
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations=3, time = 5, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations=3,time = 5)
@Threads(4)
@Fork(1)
@State(Scope.Benchmark)
public class Myclass {
   Random random = new Random();
   ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current();

   @Benchmark
   public int measureRandom(){
       return random.nextInt();
   }
   @Benchmark
   public int threadLocalmeasureRandom(){
       return threadLocalRandom.nextInt();
   }
	
}

執行結果如下圖所示,最左邊是併發執行緒的數量:

顯而易見,無論執行緒數量是多少,ThreadLocalRandom效能是遠高於Random

總結

本文講解了JDK中提供的兩種生成隨機數的方式,一個是JDK 1.0引入的Random類,另外一個是JDK1.7引入的ThreadLocalRandom類,由於底層的實現機制不同,ThreadLocalRandom的效能是遠高於Random,建議後面大家在技術選型的時候優先使用ThreadLocalRandom

如果本文對你有幫助的話,請留下一個贊吧
歡迎關注個人公眾號——JAVA旭陽
更多學習資料請移步:程式設計師成神之路

相關文章