Java隨機數探祕
本文的前3節參考修改自微信公眾號「咖啡拿鐵」的文章,感謝李釗同學對這個話題熱情的討論。
1 前言
一提到 Java 中的隨機數,很多人就會想到 Ramdom
,當出現生成隨機數這樣需求時,大多數人都會選擇使用 Random 來生成隨機數。Random 類是執行緒安全的,但其內部使用 CAS 來保證執行緒安全性,在多執行緒併發的情況下的時候它的表現是存在優化空間的。在 JDK1.7 之後,Java 提供了更好的解決方案 ThreadLocalRandom,接下來,我們一起探討下這幾個隨機數生成器的實現到底有何不同。
2 Random
Random 這個類是 JDK 提供的用來生成隨機數的一個類,這個類並不是真正的隨機,而是偽隨機,偽隨機的意思是生成的隨機數其實是有一定規律的,而這個規律出現的週期隨著偽隨機演算法的優劣而不同,一般來說週期比較長,但是可以預測。通過下面的程式碼我們可以對 Random 進行簡單的使用:
Random原理
Random 中的方法比較多,這裡就針對比較常見的 nextInt() 和 nextInt(int bound) 方法進行分析,前者會計算出 int 範圍內的隨機數,後者如果我們傳入 10,那麼他會求出 [0,10) 之間的 int 型別的隨機數,左閉右開。我們首先看一下 Random() 的構造方法:
可以發現在構造方法當中,根據當前時間的種子生成了一個 AtomicLong 型別的 seed,這也是我們後續的關鍵所在。
####nextInt()
nextInt() 的程式碼如下所示:
這個裡面直接呼叫的是 next() 方法,傳入的 32,代指的是 Int 型別的位數。
這裡會根據 seed 當前的值,通過一定的規則(偽隨機演算法)算出下一個 seed,然後進行 CAS,如果 CAS 失敗則繼續迴圈上面的操作。最後根據我們需要的 bit 位數來進行返回。核心便是 CAS 演算法。
nextInt(int bound)
nextInt(int bound) 的程式碼如下所示:
這個流程比 nextInt() 多了幾步,具體步驟如下:
- 首先獲取 31 位的隨機數,注意這裡是 31 位,和上面 32 位不同,因為在 nextInt() 方法中可以獲取到隨機數可能是負數,而 nextInt(int bound) 規定只能獲取到 [0,bound) 之前的隨機數,也就意味著必須是正數,預留一位符號位,所以只獲取了31位。(不要想著使用取絕對值這樣操作,會導致效能下降)
- 然後進行取 bound 操作。
- 如果 bound 是2的冪次方,可以直接將第一步獲取的值乘以 bound 然後右移31位,解釋一下:如果 bound 是4,那麼乘以4其實就是左移2位,其實就是變成了33位,再右移31位的話,就又會變成2位,最後,2位 int 的範圍其實就是 [0,4) 了。
- 如果不是 2 的冪,通過模運算進行處理。
併發瓶頸
在我之前的文章中就有相關的介紹,一般而言,CAS 相比加鎖有一定的優勢,但並不一定意味著高效。一個立刻被想到的解決方案是每次使用 Random 時都去 new 一個新的執行緒私有化的 Random 物件,或者使用 ThreadLocal 來維護執行緒私有化物件,但除此之外還存在更高效的方案,下面便來介紹本文的主角 ThreadLocalRandom。
3 ThreadLocalRandom
在 JDK1.7 之後提供了新的類 ThreadLocalRandom 用來在併發場景下代替 Random。使用方法比較簡單:
ThreadLocalRandom.current().nextInt(); ThreadLocalRandom.current().nextInt(10);
在 current 方法中有:
可以看見如果沒有初始化會對其進行初始化,而這裡我們的 seed 不再是一個全域性變數,在我們的Thread中有三個變數:
- threadLocalRandomSeed:ThreadLocalRandom 使用它來控制隨機數種子。
- threadLocalRandomProbe:ThreadLocalRandom 使用它來控制初始化。
- threadLocalRandomSecondarySeed:二級種子。
可以看見所有的變數都加了 @sun.misc.Contended 這個註解,用來處理偽共享問題。
在 nextInt() 方法當中程式碼如下:
我們的關鍵程式碼如下:
UNSAFE.putLong(t = Thread.currentThread(), SEED,r=UNSAFE.getLong(t, SEED) + GAMMA);
可以看見由於我們每個執行緒各自都維護了種子,這個時候並不需要 CAS,直接進行 put,在這裡利用執行緒之間隔離,減少了併發衝突;相比較 ThreadLocal<Random>
,ThreadLocalRandom 不僅僅減少了物件維護的成本,其內部實現也更輕量級。所以 ThreadLocalRandom 效能很高。
4 效能測試
除了文章中詳細介紹的 Random,ThreadLocalRandom,我還將 netty4 實現的 ThreadLocalRandom,以及 ThreadLocal<Random>
作為參考物件,一起參與 JMH 測評。
@BenchmarkMode({Mode.AverageTime}) @OutputTimeUnit(TimeUnit.NANOSECONDS) @Warmup(iterations = 3, time = 5) @Measurement(iterations = 3, time = 5) @Threads(50) @Fork(1) @State(Scope.Benchmark) public class RandomBenchmark { Random random = new Random(); ThreadLocal<Random> threadLocalRandomHolder = ThreadLocal.withInitial(Random::new); @Benchmark public int random() { return random.nextInt(); } @Benchmark public int threadLocalRandom() { return ThreadLocalRandom.current().nextInt(); } @Benchmark public int threadLocalRandomHolder() { return threadLocalRandomHolder.get().nextInt(); } @Benchmark public int nettyThreadLocalRandom() { return io.netty.util.internal.ThreadLocalRandom.current().nextInt(); } public static void main(String[] args) throws RunnerException { Options opt = new OptionsBuilder() .include(RandomBenchmark.class.getSimpleName()) .build(); new Runner(opt).run(); } }
測評結果如下:
BenchmarkModeCntScoreErrorUnits RandomBenchmark.nettyThreadLocalRandomavgt3192.202 ± 295.897ns/op RandomBenchmark.randomavgt33197.620 ± 380.981ns/op RandomBenchmark.threadLocalRandomavgt390.731 ±39.098ns/op RandomBenchmark.threadLocalRandomHolderavgt3229.502 ± 267.144ns/op
從上圖可以發現,JDK1.7 的 ThreadLocalRandom
取得了最好的成績,僅僅需要 90 ns 就可以生成一次隨機數,netty 實現的 ThreadLocalRandom
以及使用 ThreadLocal 維護 Random 的方式差距不是很大,位列 2、3 位,共享的 Random 變數則效果最差。
可見,在併發場景下,ThreadLocalRandom 可以明顯的提升效能。
5 注意點
注意,ThreadLocalRandom 切記不要呼叫 current 方法之後,作為共享變數使用
public class WrongCase { ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current(); public int concurrentNextInt(){ return threadLocalRandom.nextInt(); } }
這是因為 ThreadLocalRandom.current() 會使用初始化它的執行緒來填充隨機種子,這會帶來導致多個執行緒使用相同的 seed。
public class Main { public static void main(String[] args) { ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current(); for(int i=0;i<10;i++) new Thread(new Runnable() { @Override public void run() { System.out.println(threadLocalRandom.nextInt()); } }).start(); } }
輸出相同的隨機數:
-1667209487 -1667209487 -1667209487 -1667209487 -1667209487 -1667209487 -1667209487 -1667209487 -1667209487 -1667209487
請在確保不同執行緒獲取不同的 seed,最簡單的方式便是每次呼叫都是使用 current():
public class RightCase { public int concurrentNextInt(){ return ThreadLocalRandom.current().nextInt(); } }
彩蛋1
樑飛部落格中一句話常常在我腦海中縈繞:魔鬼在細節中。優秀的程式碼都是一個個小細節堆砌出來,今天介紹的 ThreadLocalRandom 也不例外。
在 incubator-dubbo-2.7.0 中,隨機負載均衡器的一個小改動便是將 Random 替換為了 ThreadLocalRandom,用於優化併發效能。
彩蛋2
ThreadLocalRandom 的 nextInt(int bound) 方法中,當 bound 不為 2 的冪次方時,使用了一個迴圈來修改 r 的值,我認為這可能不必要,你覺得呢?
public int nextInt(int bound) { if (bound <= 0) throw new IllegalArgumentException(BadBound); int r = mix32(nextSeed()); int m = bound - 1; if ((bound & m) == 0) // power of two r &= m; else { // reject over-represented candidates for (int u = r >>> 1; u + m - (r = u % bound) < 0; u = mix32(nextSeed()) >>> 1) ; } return r; }
歡迎關注李釗同學的微信公眾號:「咖啡拿鐵」
當然,也歡迎關注我的微信公眾號:「Kirito的技術分享」,關於文章的任何疑問都會得到回覆,帶來更多 Java 相關的技術分享。