揭祕Java高效隨機數生成器

咖啡拿鐵發表於2018-08-30

1.前言

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

2.Random

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

揭祕Java高效隨機數生成器

2.1Random原理

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

揭祕Java高效隨機數生成器

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

2.1.1 nextInt()

在nextInt()中程式碼如下:

揭祕Java高效隨機數生成器

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

揭祕Java高效隨機數生成器

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

2.1.2 nextInt(int bound)

在nextInt(int bound)中程式碼如下:

揭祕Java高效隨機數生成器

這個流程比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操作,當併發執行緒越多的時候,其效能肯定越低。

偽共享:有關於偽共享和快取行的描述可以看我的你應該知道的高效能無鎖佇列Disruptor,對於AtomicLong中的value並沒有處理快取行

3.ThreadLocalRandom

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

揭祕Java高效隨機數生成器

在current方法中有:

揭祕Java高效隨機數生成器
可以看見如果沒有初始化會對其進行初始化,而這裡我們的seed不再是一個全域性變數,在我們的Thread中有三個變數:
揭祕Java高效隨機數生成器

  • threadLocalRandomSeed:這個是我們用來控制隨機數的種子。
  • threadLocalRandomProbe:這個是ThreadLocalRandom用來控制初始化。
  • threadLocalRandomSecondarySeed:這個是二級種子。

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

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

揭祕Java高效隨機數生成器

我們的關鍵程式碼如下:

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高效隨機數生成器

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

最後

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

最後這篇文章被我收錄於JGrowing,一個全面,優秀,由社群一起共建的Java學習路線,如果您想參與開源專案的維護,可以一起共建,github地址為:github.com/javagrowing… 麻煩給個小星星喲。

如果你覺得這篇文章對你有文章,可以關注我的技術公眾號,最近作者收集了很多最新的學習資料視訊以及面試資料,關注之後即可領取,你的關注和轉發是對我最大的支援,O(∩_∩)O

揭祕Java高效隨機數生成器

相關文章