天天看點

揭秘Java高效随機數生成器1.前言2.Random3.ThreadLocalRandom4.性能資料最後

1.前言

在Java中一提到随機數,很多人就會想到Random類,如果有生成随機數的需求的時候,大多數時候都會選擇使用Random來進行随機數生成,雖然其内部使用CAS來實作,但是在多線程并發的情況下的時候它的表現并不是很好。在JDK1.7之後,JDK提供了提供了更好的解決方案,接下來讓我們一起學習下到底為什麼Random會慢?又是怎麼解決的呢?

2.Random

Random這個類是JDK提供的用來生成随機數的一個類,這個類并不是真正的随機,而是僞随機,僞随機的意思是生成的随機數其實是有一定規律的,而這個規律出現的周期随着僞随機算法的優劣而不同,一般來說周期比較長,但是可以預測。通過下面的代碼我們可以對Ramdom進行簡單的使用:

揭秘Java高效随機數生成器1.前言2.Random3.ThreadLocalRandom4.性能資料最後

2.1Ramdom原理

Ramdom中的方法比較多,這裡就針對比較常見的nextInt()和nextInt(int bound)方法進行分析,前者會計算出int範圍内随機數,後者如果我們傳入10,那麼他會求出[0,10)之間的int類型的随機數,左閉右開。在具體分析之前我們先看一下Ramdom()的構造方法:

揭秘Java高效随機數生成器1.前言2.Random3.ThreadLocalRandom4.性能資料最後

可以看見在構造方法當中根據目前時間的種子生成了一個AtomicLong類型的seed,這也是我們後續的關鍵所在。

2.1.1 nextInt()

在nextInt()中代碼如下:

揭秘Java高效随機數生成器1.前言2.Random3.ThreadLocalRandom4.性能資料最後

這個裡面直接調用的是next()方法,傳入的32,這裡的32指的是Int的位數。

揭秘Java高效随機數生成器1.前言2.Random3.ThreadLocalRandom4.性能資料最後

這裡會根據seed目前的值,通過一定的規則(僞随機)算出下一個seed,然後進行cas,如果cas失敗繼續循環上面的操作。最後根據我們需要的bit位數來進行傳回。

2.1.2 nextInt(int bound)

在nextInt(int bound)中代碼如下:

揭秘Java高效随機數生成器1.前言2.Random3.ThreadLocalRandom4.性能資料最後

這個流程比nextInt()多了幾步,具體步驟如下:

  1. 首先擷取31位的随機數,注意這裡是31位,和上面32位不同,因為在nextInt()方法中可以擷取到負數的随機數,而nextInt(int bound)規定隻能擷取到[0,bound)之前的随機數,也就是必須是正數,而int的第一位是符号位是以隻擷取了31位。
  2. 然後進行取bound操作。
  3. 如果bound是2的幂,那麼直接将第一步擷取的資料乘以bound然後右移31位,解釋一下:如果bound是4那麼,如果乘以4其實就是左移2位,那麼其實就是變成了33位,那麼再右移31位的話,就又會變成2位,那麼2位的int的大小範圍其實就是[0,4)了。
  4. 如果不是2的幂,通過取餘的操作進行處理。

2.1.3 并發瓶頸

CAS: 可以看見在next(int bits)方法中,對AtomicLong進行CAS操作,如果失敗則會對其進行循環重試。很多人一看見CAS,因為其不需要加鎖,是以馬上就想到高性能,高并發。但是在這裡,他卻成為了我們多線程并發性能的瓶頸,可以想象當我們多個線程都進行CAS的時候必定隻有一個失敗其他的繼續會循環做CAS操作,當并發線程越多的時候,其性能肯定越低。

僞共享:有關于僞共享和緩存行的描述可以看我的還在用BlockingQueue?讀這篇文章,了解下Disruptor吧,對于AtomicLong中的value并沒有處理緩存行

3.ThreadLocalRandom

在JDK1.7之後提供了新的類ThreadLocalRandom用來代替Ramdom。使用方法比較簡單:

揭秘Java高效随機數生成器1.前言2.Random3.ThreadLocalRandom4.性能資料最後

在current方法中有:

揭秘Java高效随機數生成器1.前言2.Random3.ThreadLocalRandom4.性能資料最後

可以看見如果沒有初始化會對其進行初始化,而這裡我們的seed不再是一個全局變量,在我們的Thread中有三個變量:

揭秘Java高效随機數生成器1.前言2.Random3.ThreadLocalRandom4.性能資料最後
  • threadLocalRandomSeed:這個是我們用來控制随機數的種子。
  • threadLocalRandomProbe:這個是ThreadLocalRandom用來控制初始化。
  • threadLocalRandomSecondarySeed:這個是二級種子。

可以看見所有的變量都加了@sun.misc.Contended這個注解,這個是用來處理僞共享的問題。

在nextInt()方法當中代碼如下:

揭秘Java高效随機數生成器1.前言2.Random3.ThreadLocalRandom4.性能資料最後

我們的關鍵代碼如下:

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

複制

可以看見由于我們每個線程各自都維護了種子,這個時候并不需要CAS,直接進行put,在這裡利用線程之間隔離,減少了并發沖突,是以ThreadLocalRandom性能很高。

4.性能資料

使用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();
   }
}           

複制

并發線程 Random ThreadLocalRandom
1 12.798 ns/op 4.690 ns/op
4 361.027 ns/op 5.930 ns/op
16 2288.391 ns/op 22.155 ns/op
32 4812.740 ns/op 49.144 ns/op
揭秘Java高效随機數生成器1.前言2.Random3.ThreadLocalRandom4.性能資料最後

可以看見ThreadLocalRandom 基本上是完虐Random,并發程度越高差距越大。

最後

相信讀完這篇文章以後,未來如果在實際應用中使用随機數你肯定會有新的選擇。