前言
大家項目中如果有生成随機數的需求,我想大多都會選擇使用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。