Java中生成随机数的4种方式!
在 Java 中,生成隨機數的場景有很多,所以本文我們就來盤點一下 4 種生成隨機數的方式,以及它們之間的區別和每種生成方式所對應的場景。
1.Random
Random 類誕生于 JDK 1.0,它產生的隨機數是偽隨機數,也就是有規則的隨機數。Random 使用的隨機算法為 linear congruential pseudorandom number generator (LGC) 線性同余法偽隨機數。在隨機數生成時,隨機算法的起源數字稱為種子數(seed),在種子數的基礎上進行一定的變換,從而產生需要的隨機數字。
Random 對象在種子數相同的情況下,相同次數生成的隨機數是相同的。比如兩個種子數相同的 Random 對象,第一次生成的隨機數字完全相同,第二次生成的隨機數字也完全相同。默認情況下 new Random() 使用的是當前納秒時間作為種子數的。
① 基礎使用
使用 Random 生成一個從 0 到 10 的隨機數(不包含 10),實現代碼如下:
// 生成 Random 對象 Random random = new Random(); for (int i = 0; i < 10; i++) {// 生成 0-9 隨機整數int number = random.nextInt(10);System.out.println("生成隨機數:" + number); } 復制代碼以上程序的執行結果為:
② 優缺點分析
Random 使用 LGC 算法生成偽隨機數的優點是執行效率比較高,生成的速度比較快。 ?
它的缺點是如果 Random 的隨機種子一樣的話,每次生成的隨機數都是可預測的(都是一樣的)。如下代碼所示,當我們給兩個線程設置相同的種子數的時候,會發現每次產生的隨機數也是相同的:
// 創建兩個線程 for (int i = 0; i < 2; i++) {new Thread(() -> {// 創建 Random 對象,設置相同的種子Random random = new Random(1024);// 生成 3 次隨機數for (int j = 0; j < 3; j++) {// 生成隨機數int number = random.nextInt();// 打印生成的隨機數System.out.println(Thread.currentThread().getName() + ":" +number);// 休眠 200 mstry {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)); // CAS(Compare and Swap)生成隨機數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 機制來實現。
2.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 對象 ThreadLocalRandom random = ThreadLocalRandom.current(); for (int i = 0; i < 10; i++) {// 生成 0-9 隨機整數int number = random.nextInt(10);// 打印結果System.out.println("生成隨機數:" + number); } 復制代碼以上程序的執行結果為:
② 實現原理
ThreadLocalRandom 的實現原理和 ThreadLocal 類似,它是讓每個線程持有自己的本地種子,該種子在生成隨機數時候才會被初始化,實現源碼如下:
public int nextInt(int bound) {// 參數效驗if (bound <= 0)throw new IllegalArgumentException(BadBound);// 根據當前線程中種子計算新種子int r = mix32(nextSeed());int m = bound - 1;// 根據新種子和 bound 計算隨機數if ((bound & m) == 0) // power of twor &= m;else { // reject over-represented candidatesfor (int u = r >>> 1;u + m - (r = u % bound) < 0;u = mix32(nextSeed()) >>> 1);}return r; }final long nextSeed() {Thread t; long r; // read and update per-thread seed// 獲取當前線程中 threadLocalRandomSeed 變量,然后在種子的基礎上累加 GAMMA 值作為新種子// 再使用 UNSAFE.putLong 將新種子存放到當前線程的 threadLocalRandomSeed 變量中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) {// only allow call from super() constructorif (initialized)throw new UnsupportedOperationException(); } 復制代碼只要程序中調用了 setSeed() 方法就會拋出 UnsupportedOperationException 異常,如下圖所示:
ThreadLocalRandom 缺點分析
雖然 ThreadLocalRandom 不支持手動設置隨機種子的方法,但并不代表 ThreadLocalRandom 就是完美的,當我們查看 ThreadLocalRandom 初始化隨機種子的方法 initialSeed() 源碼時發現,默認情況下它的隨機種子也是以當前時間有關,源碼如下:
private static long initialSeed() {// 嘗試獲取 JVM 的啟動參數String sec = VM.getSavedProperty("java.util.secureRandomSeed");// 如果啟動參數設置的值為 true,則參數一個隨機 8 位的種子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 會產生一個隨機種子,一定程度上能緩解隨機種子相同所帶來隨機數可預測的問題,然而默認情況下如果不設置此參數,那么在多線程中就可以因為啟動時間相同,而導致多個線程在每一步操作中都會生成相同的隨機數。
3.SecureRandom
SecureRandom 繼承自 Random,該類提供加密強隨機數生成器。SecureRandom 不同于 Random,它收集了一些隨機事件,比如鼠標點擊,鍵盤點擊等,SecureRandom 使用這些隨機事件作為種子。這意味著,種子是不可預測的,而不像 Random 默認使用系統當前時間的毫秒數作為種子,從而避免了生成相同隨機數的可能性。 ?
基礎使用
// 創建 SecureRandom 對象,并設置加密算法 SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); for (int i = 0; i < 10; i++) {// 生成 0-9 隨機整數int number = random.nextInt(10);// 打印結果System.out.println("生成隨機數:" + number); } 復制代碼以上程序的執行結果為: SecureRandom 默認支持兩種加密算法:
當然除了上述的操作方式之外,你還可以選擇使用 new SecureRandom() 來創建 SecureRandom 對象,實現代碼如下:
SecureRandom secureRandom = new SecureRandom(); 復制代碼通過 new 初始化 SecureRandom,默認會使用 NativePRNG 算法來生成隨機數,但是也可以配置 JVM 啟動參數“-Djava.security”參數來修改生成隨機數的算法,或選擇使用 getInstance("算法名稱") 的方式來指定生成隨機數的算法。
4.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++) {// 生成一個從 0-99 的整數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(); }private static final class RandomNumberGeneratorHolder {static final Random randomNumberGenerator = new Random(); } 復制代碼總結
本文我們介紹了 4 種生成隨機數的方法,其中 Math 是對 Random 的封裝,所以二者比較類似。Random 生成的是偽隨機數,是以當前納秒時間作為種子數的,并且在多線程競爭比較激烈的情況下因為要進行 CAS 操作,所以存在一定的性能問題,但對于絕大數應用場景來說,使用 Random 已經足夠了。當在競爭比較激烈的場景下可以使用 ThreadLocalRandom 來替代 Random,但如果對安全性要求比較高的情況下,可以使用 SecureRandom 來生成隨機數,因為 SecureRandom 會收集一些隨機事件來作為隨機種子,所以 SecureRandom 可以看作是生成真正隨機數的一個工具類。
作者:Java中文社群
鏈接:https://juejin.cn/post/6973816615209009159
來源:掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
總結
以上是生活随笔為你收集整理的Java中生成随机数的4种方式!的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 就这一次看懂TraceView
- 下一篇: 网络编程:TCP简单实现群聊功能