揭祕Java高效隨機數生成器

1.前言

在Java中一提到隨機數,不少人就會想到Ramdom類,若是有生成隨機數的需求的時候,大多數時候都會選擇使用Random來進行隨機數生成,雖然其內部使用CAS來實現,可是在多線程併發的狀況下的時候它的表現並非很好。在JDK1.7以後,JDK提供了提供了更好的解決方案,接下來讓咱們一塊兒學習下到底爲何Ramdom會慢?又是怎麼解決的呢?java

2.Random

Random這個類是JDK提供的用來生成隨機數的一個類,這個類並非真正的隨機,而是僞隨機,僞隨機的意思是生成的隨機數實際上是有必定規律的,而這個規律出現的週期隨着僞隨機算法的優劣而不一樣,通常來講週期比較長,可是能夠預測。經過下面的代碼咱們能夠對Random進行簡單的使用: git

2.1Random原理

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

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

2.1.1 nextInt()

在nextInt()中代碼以下: 算法

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

這裏會根據seed當前的值,經過必定的規則(僞隨機)算出下一個seed,而後進行cas,若是cas失敗繼續循環上面的操做。最後根據咱們須要的bit位數來進行返回。bash

2.1.2 nextInt(int bound)

在nextInt(int bound)中代碼以下: 多線程

這個流程比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操做,當併發線程越多的時候,其性能確定越低。dom

僞共享:有關於僞共享和緩存行的描述能夠看個人你應該知道的高性能無鎖隊列Disruptor,對於AtomicLong中的value並無處理緩存行

3.ThreadLocalRandom

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

在current方法中有:

能夠看見若是沒有初始化會對其進行初始化,而這裏咱們的seed再也不是一個全局變量,在咱們的Thread中有三個變量:

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

能夠看見全部的變量都加了@sun.misc.Contended這個註解,這個是用來處理僞共享的問題。

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

咱們的關鍵代碼以下:

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

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

最後

相信讀完這篇文章之後,將來若是在實際應用中使用隨機數你確定會有新的選擇。

最後這篇文章被我收錄於JGrowing,一個全面,優秀,由社區一塊兒共建的Java學習路線,若是您想參與開源項目的維護,能夠一塊兒共建,github地址爲:github.com/javagrowing… 麻煩給個小星星喲。

若是你以爲這篇文章對你有文章,能夠關注個人技術公衆號,最近做者收集了不少最新的學習資料視頻以及面試資料,關注以後便可領取,你的關注和轉發是對我最大的支持,O(∩_∩)O

相關文章
相關標籤/搜索