在 Java 中,生成隨機數的場景有很多,所以本文我們就來盤點一下 4 種生成隨機數的方式,以及它們之間的區別和每種生成方式所對應的場景。
Random 類誕生於 JDK 1.0,它產生的隨機數是偽隨機數,也就是有規則的隨機數。Random 使用的隨機算法為 linear congruential pseudorandom number generator (LGC) 線性同餘法偽隨機數。在隨機數生成時,隨機算法的起源數字稱為種子數(seed),在種子數的基礎上進行一定的變換,從而產生需要的隨機數字。
Random 對象在種子數相同的情況下,相同次數生成的隨機數是相同的。比如兩個種子數相同的 Random 對象,第一次生成的隨機數字完全相同,第二次生成的隨機數字也完全相同。默認情況下 new Random() 使用的是當前納秒時間作為種子數的。
① 基礎使用使用 Random 生成一個從 0 到 10 的隨機數(不包含 10),實現代碼如下:
Random random = new Random();for (int i = 0; i < 10; i++) { int number = random.nextInt(10); System.out.println("生成隨機數:" + number);}以上程序的執行結果為:
② 優缺點分析Random 使用 LGC 算法生成偽隨機數的優點是執行效率比較高,生成的速度比較快。
它的缺點是如果 Random 的隨機種子一樣的話,每次生成的隨機數都是可預測的(都是一樣的)。如下代碼所示,當我們給兩個線程設置相同的種子數的時候,會發現每次產生的隨機數也是相同的:
for (int i = 0; i < 2; i++) { new Thread(() -> { Random random = new Random(1024); for (int j = 0; j < 3; j++) { int number = random.nextInt(); System.out.println(Thread.currentThread().getName() + ":" + number); try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("-"); } }).start();}以上程序的執行結果為:③ 線程安全問題當我們要使用一個類時,我們首先關心的第一個問題是:它是否為線程安全?對於 Random 來說,Random 是線程安全的。
PS:線程安全指的是在多線程的場景下,程序的執行結果和預期的結果一致,就叫線程安全的,否則則為非線程安全的(也叫線程安全問題)。比如有兩個線程,第一個線程執行 10 萬次 ++ 操作,第二個線程執行 10 萬次 -- 操作,那麼最終的結果應該是沒加也沒減,如果程序最終的結果和預期不符,則為非線程安全的。
我們來看 Random 的實現源碼:
public Random() { this(seedUniquifier() ^ System.nanoTime());}
public int nextInt() { return next(32);}
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));}PS:本文所有源碼來自於 JDK 1.8.0_211。
從以上源碼可以看出,Random 底層使用的是 CAS(Compare and Swap,比較並替換)來解決線程安全問題的,因此對於絕大數隨機數生成的場景,使用 Random 不乏為一種很好的選擇。
PS:Java 並發機制實現原子操作有兩種:一種是鎖,一種是 CAS。
CAS 是 Compare And Swap(比較並替換)的縮寫,java.util.concurrent.atomic 中的很多類,如(AtomicInteger AtomicBoolean AtomicLong等)都使用了 CAS 機制來實現。
ThreadLocalRandom
ThreadLocalRandom 是 JDK 1.7 新提供的類,它屬於 JUC(java.util.concurrent)下的一員,為什麼有了 Random 之後還會再創建一個 ThreadLocalRandom?
原因很簡單,通過上面 Random 的源碼我們可以看出,Random 在生成隨機數時使用的 CAS 來解決線程安全問題的,然而 CAS 在線程競爭比較激烈的場景中效率是非常低的,原因是 CAS 對比時老有其他的線程在修改原來的值,所以導致 CAS 對比失敗,所以它要一直循環來嘗試進行 CAS 操作。所以在多線程競爭比較激烈的場景可以使用 ThreadLocalRandom 來解決 Random 執行效率比較低的問題。
當我們第一眼看到 ThreadLocalRandom 的時候,一定會聯想到一次類 ThreadLocal,確實如此。ThreadLocalRandom 的實現原理與 ThreadLocal 類似,它相當於給每個線程一個自己的本地種子,從而就可以避免因多個線程競爭一個種子,而帶來的額外性能開銷了。
① 基礎使用接下來我們使用 ThreadLocalRandom 來生成一個 0 到 10 的隨機數(不包含 10),實現代碼如下:
ThreadLocalRandom random = ThreadLocalRandom.current();for (int i = 0; i < 10; i++) { int number = random.nextInt(10); System.out.println("生成隨機數:" + number);}以上程序的執行結果為:② 實現原理ThreadLocalRandom 的實現原理和 ThreadLocal 類似,它是讓每個線程持有自己的本地種子,該種子在生成隨機數時候才會被初始化,實現源碼如下:
public int nextInt(int bound) { if (bound <= 0) thrownew IllegalArgumentException(BadBound); int r = mix32(nextSeed()); int m = bound - 1; if ((bound & m) == 0) r &= m; else { for (int u = r >>> 1; u + m - (r = u % bound) < 0; u = mix32(nextSeed()) >>> 1) ; } return r;}
final long nextSeed() { Thread t; long r; UNSAFE.putLong(t = Thread.currentThread(), SEED, r = UNSAFE.getLong(t, SEED) + GAMMA); return r;}③ 優缺點分析ThreadLocalRandom 結合了 Random 和 ThreadLocal 類,並被隔離在當前線程中。因此它通過避免競爭操作種子數,從而在多線程運行的環境中實現了更好的性能,而且也保證了它的線程安全。
另外,不同於 Random, ThreadLocalRandom 明確不支持設置隨機種子。它重寫了 Random 的setSeed(long seed) 方法並直接拋出了 UnsupportedOperationException 異常,因此降低了多個線程出現隨機數重複的可能性。
源碼如下:
public void setSeed(long seed) { if (initialized) thrownew UnsupportedOperationException();}只要程序中調用了 setSeed() 方法就會拋出 UnsupportedOperationException 異常,如下圖所示:
ThreadLocalRandom 缺點分析雖然 ThreadLocalRandom 不支持手動設置隨機種子的方法,但並不代表 ThreadLocalRandom 就是完美的,當我們查看 ThreadLocalRandom 初始化隨機種子的方法 initialSeed() 源碼時發現,默認情況下它的隨機種子也是以當前時間有關,源碼如下:
private static long initialSeed() { String sec = VM.getSavedProperty("java.util.secureRandomSeed"); if (Boolean.parseBoolean(sec)) { byte[] seedBytes = java.security.SecureRandom.getSeed(8); long s = (long)(seedBytes[0]) & 0xffL; for (int i = 1; i < 8; ++i) s = (s << 8) | ((long)(seedBytes[i]) & 0xffL); return s; } return (mix64(System.currentTimeMillis()) ^ mix64(System.nanoTime()));}從上述源碼可以看出,當我們設置了啟動參數「-Djava.util.secureRandomSeed=true」時,ThreadLocalRandom 會產生一個隨機種子,一定程度上能緩解隨機種子相同所帶來隨機數可預測的問題,然而默認情況下如果不設置此參數,那麼在多線程中就可以因為啟動時間相同,而導致多個線程在每一步操作中都會生成相同的隨機數。
SecureRandom
SecureRandom 繼承自 Random,該類提供加密強隨機數生成器。SecureRandom 不同於 Random,它收集了一些隨機事件,比如滑鼠點擊,鍵盤點擊等,SecureRandom 使用這些隨機事件作為種子。這意味著,種子是不可預測的,而不像 Random 默認使用系統當前時間的毫秒數作為種子,從而避免了生成相同隨機數的可能性。
基礎使用SecureRandom random = SecureRandom.getInstance("SHA1PRNG");for (int i = 0; i < 10; i++) { int number = random.nextInt(10); System.out.println("生成隨機數:" + number);}以上程序的執行結果為:SecureRandom 默認支持兩種加密算法:
SHA1PRNG 算法,提供者 sun.security.provider.SecureRandom;NativePRNG 算法,提供者 sun.security.provider.NativePRNG。當然除了上述的操作方式之外,你還可以選擇使用 new SecureRandom() 來創建 SecureRandom 對象,實現代碼如下:
SecureRandom secureRandom = new SecureRandom();通過 new 初始化 SecureRandom,默認會使用 NativePRNG 算法來生成隨機數,但是也可以配置 JVM 啟動參數「-Djava.security」參數來修改生成隨機數的算法,或選擇使用 getInstance("算法名稱") 的方式來指定生成隨機數的算法。
Math
Math 類誕生於 JDK 1.0,它裡面包含了用於執行基本數學運算的屬性和方法,如初等指數、對數、平方根和三角函數,當然它裡面也包含了生成隨機數的靜態方法 Math.random() ,此方法會產生一個 0 到 1 的 double 值,如下代碼所示。
① 基礎使用for (int i = 0; i < 10; i++) { double number = Math.random(); System.out.println("生成隨機數:" + number);}以上程序的執行結果為:
② 擴展當然如果你想用它來生成一個一定範圍的 int 值也是可以的,你可以這樣寫:
for (int i = 0; i < 10; i++) { int number = (int) (Math.random() * 100); System.out.println("生成隨機數:" + number);}以上程序的執行結果為:
③ 實現原理通過分析 Math 的源碼我們可以得知:當第一次調用 Math.random() 方法時,自動創建了一個偽隨機數生成器,實際上用的是 new java.util.Random(),當下一次繼續調用 Math.random() 方法時,就會使用這個新的偽隨機數生成器。
源碼如下:
public static double random() { return RandomNumberGeneratorHolder.randomNumberGenerator.nextDouble();}
privatestaticfinalclass RandomNumberGeneratorHolder { staticfinal Random randomNumberGenerator = new Random();}
總結
本文我們介紹了 4 種生成隨機數的方法,其中 Math 是對 Random 的封裝,所以二者比較類似。Random 生成的是偽隨機數,是以當前納秒時間作為種子數的,並且在多線程競爭比較激烈的情況下因為要進行 CAS 操作,所以存在一定的性能問題,但對於絕大數應用場景來說,使用 Random 已經足夠了。當在競爭比較激烈的場景下可以使用 ThreadLocalRandom 來替代 Random,但如果對安全性要求比較高的情況下,可以使用 SecureRandom 來生成隨機數,因為 SecureRandom 會收集一些隨機事件來作為隨機種子,所以 SecureRandom 可以看作是生成真正隨機數的一個工具類。
參考 & 鳴謝:
☞Windows 11 預覽版洩露!有 macOS 那味兒了.
☞賈伯斯居然是這樣面試我的,你能挺到哪一步?