Java中生成隨機(jī)數(shù)的4種方式與區(qū)別詳解
在 Java 中,生成隨機(jī)數(shù)的場景有很多,所以本文我們就來盤點一下 4 種生成隨機(jī)數(shù)的方式,以及它們之間的區(qū)別和每種生成方式所對應(yīng)的場景。
1.Random
Random 類誕生于 JDK 1.0,它產(chǎn)生的隨機(jī)數(shù)是偽隨機(jī)數(shù),也就是有規(guī)則的隨機(jī)數(shù)。Random 使用的隨機(jī)算法為 linear congruential pseudorandom number generator (LGC) 線性同余法偽隨機(jī)數(shù)。在隨機(jī)數(shù)生成時,隨機(jī)算法的起源數(shù)字稱為種子數(shù)(seed),在種子數(shù)的基礎(chǔ)上進(jìn)行一定的變換,從而產(chǎn)生需要的隨機(jī)數(shù)字。
Random 對象在種子數(shù)相同的情況下,相同次數(shù)生成的隨機(jī)數(shù)是相同的。比如兩個種子數(shù)相同的 Random 對象,第一次生成的隨機(jī)數(shù)字完全相同,第二次生成的隨機(jī)數(shù)字也完全相同。默認(rèn)情況下 new Random() 使用的是當(dāng)前納秒時間作為種子數(shù)的。
① 基礎(chǔ)使用
使用 Random 生成一個從 0 到 10 的隨機(jī)數(shù)(不包含 10),實現(xiàn)代碼如下:
// 生成 Random 對象 Random random = new Random(); for (int i = 0; i < 10; i++) { // 生成 0-9 隨機(jī)整數(shù) int number = random.nextInt(10); System.out.println("生成隨機(jī)數(shù):" + number); }
以上程序的執(zhí)行結(jié)果為:
② 優(yōu)缺點分析
Random 使用 LGC 算法生成偽隨機(jī)數(shù)的優(yōu)點是執(zhí)行效率比較高,生成的速度比較快。
它的缺點是如果 Random 的隨機(jī)種子一樣的話,每次生成的隨機(jī)數(shù)都是可預(yù)測的(都是一樣的)。如下代碼所示,當(dāng)我們給兩個線程設(shè)置相同的種子數(shù)的時候,會發(fā)現(xiàn)每次產(chǎn)生的隨機(jī)數(shù)也是相同的:
// 創(chuàng)建兩個線程 for (int i = 0; i < 2; i++) { new Thread(() -> { // 創(chuàng)建 Random 對象,設(shè)置相同的種子 Random random = new Random(1024); // 生成 3 次隨機(jī)數(shù) for (int j = 0; j < 3; j++) { // 生成隨機(jī)數(shù) int number = random.nextInt(); // 打印生成的隨機(jī)數(shù) System.out.println(Thread.currentThread().getName() + ":" + number); // 休眠 200 ms try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("---------------------"); } }).start(); }
以上程序的執(zhí)行結(jié)果為:
③ 線程安全問題
當(dāng)我們要使用一個類時,我們首先關(guān)心的第一個問題是:它是否為線程安全?對于 Random 來說,Random 是線程安全的。
PS:線程安全指的是在多線程的場景下,程序的執(zhí)行結(jié)果和預(yù)期的結(jié)果一致,就叫線程安全的,否則則為非線程安全的(也叫線程安全問題)。比如有兩個線程,第一個線程執(zhí)行 10 萬次 ++ 操作,第二個線程執(zhí)行 10 萬次 -- 操作,那么最終的結(jié)果應(yīng)該是沒加也沒減,如果程序最終的結(jié)果和預(yù)期不符,則為非線程安全的。
我們來看 Random 的實現(xiàn)源碼:
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)生成隨機(jī)數(shù) return (int)(nextseed >>> (48 - bits)); }
PS:本文所有源碼來自于 JDK 1.8.0_211。
從以上源碼可以看出,Random 底層使用的是 CAS(Compare and Swap,比較并替換)來解決線程安全問題的,因此對于絕大數(shù)隨機(jī)數(shù)生成的場景,使用 Random 不乏為一種很好的選擇。
PS:Java 并發(fā)機(jī)制實現(xiàn)原子操作有兩種:一種是鎖,一種是 CAS。
CAS 是 Compare And Swap(比較并替換)的縮寫,java.util.concurrent.atomic 中的很多類,如(AtomicInteger AtomicBoolean AtomicLong等)都使用了 CAS 機(jī)制來實現(xiàn)。
2.ThreadLocalRandom
ThreadLocalRandom 是 JDK 1.7 新提供的類,它屬于 JUC(java.util.concurrent)下的一員,為什么有了 Random 之后還會再創(chuàng)建一個 ThreadLocalRandom?
原因很簡單,通過上面 Random 的源碼我們可以看出,Random 在生成隨機(jī)數(shù)時使用的 CAS 來解決線程安全問題的,然而** CAS 在線程競爭比較激烈的場景中效率是非常低的**,原因是 CAS 對比時老有其他的線程在修改原來的值,所以導(dǎo)致 CAS 對比失敗,所以它要一直循環(huán)來嘗試進(jìn)行 CAS 操作。所以在多線程競爭比較激烈的場景可以使用 ThreadLocalRandom 來解決 Random 執(zhí)行效率比較低的問題。
當(dāng)我們第一眼看到 ThreadLocalRandom 的時候,一定會聯(lián)想到一次類 ThreadLocal,確實如此。ThreadLocalRandom 的實現(xiàn)原理與 ThreadLocal 類似,它相當(dāng)于給每個線程一個自己的本地種子,從而就可以避免因多個線程競爭一個種子,而帶來的額外性能開銷了。
① 基礎(chǔ)使用
接下來我們使用 ThreadLocalRandom 來生成一個 0 到 10 的隨機(jī)數(shù)(不包含 10),實現(xiàn)代碼如下:
// 得到 ThreadLocalRandom 對象 ThreadLocalRandom random = ThreadLocalRandom.current(); for (int i = 0; i < 10; i++) { // 生成 0-9 隨機(jī)整數(shù) int number = random.nextInt(10); // 打印結(jié)果 System.out.println("生成隨機(jī)數(shù):" + number); }
以上程序的執(zhí)行結(jié)果為:
② 實現(xiàn)原理
ThreadLocalRandom 的實現(xiàn)原理和 ThreadLocal 類似,它是讓每個線程持有自己的本地種子,該種子在生成隨機(jī)數(shù)時候才會被初始化,實現(xiàn)源碼如下:
public int nextInt(int bound) { // 參數(shù)效驗 if (bound <= 0) throw new IllegalArgumentException(BadBound); // 根據(jù)當(dāng)前線程中種子計算新種子 int r = mix32(nextSeed()); int m = bound - 1; // 根據(jù)新種子和 bound 計算隨機(jī)數(shù) 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; } final long nextSeed() { Thread t; long r; // read and update per-thread seed // 獲取當(dāng)前線程中 threadLocalRandomSeed 變量,然后在種子的基礎(chǔ)上累加 GAMMA 值作為新種子 // 再使用 UNSAFE.putLong 將新種子存放到當(dāng)前線程的 threadLocalRandomSeed 變量中 UNSAFE.putLong(t = Thread.currentThread(), SEED, r = UNSAFE.getLong(t, SEED) + GAMMA); return r; }
③ 優(yōu)缺點分析
ThreadLocalRandom 結(jié)合了 Random 和 ThreadLocal 類,并被隔離在當(dāng)前線程中。因此它通過避免競爭操作種子數(shù),從而在多線程運(yùn)行的環(huán)境中實現(xiàn)了更好的性能,而且也保證了它的線程安全。
另外,不同于 Random, ThreadLocalRandom 明確不支持設(shè)置隨機(jī)種子。它重寫了 Random 的
setSeed(long seed) 方法并直接拋出了 UnsupportedOperationException 異常,因此降低了多個線程出現(xiàn)隨機(jī)數(shù)重復(fù)的可能性。
源碼如下:
public void setSeed(long seed) { // only allow call from super() constructor if (initialized) throw new UnsupportedOperationException(); }
只要程序中調(diào)用了 setSeed() 方法就會拋出 UnsupportedOperationException 異常,如下圖所示:
ThreadLocalRandom 缺點分析
雖然 ThreadLocalRandom 不支持手動設(shè)置隨機(jī)種子的方法,但并不代表 ThreadLocalRandom 就是完美的,當(dāng)我們查看 ThreadLocalRandom 初始化隨機(jī)種子的方法 initialSeed() 源碼時發(fā)現(xiàn),默認(rèn)情況下它的隨機(jī)種子也是以當(dāng)前時間有關(guān),源碼如下:
private static long initialSeed() { // 嘗試獲取 JVM 的啟動參數(shù) String sec = VM.getSavedProperty("java.util.secureRandomSeed"); // 如果啟動參數(shù)設(shè)置的值為 true,則參數(shù)一個隨機(jī) 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; } // 如果沒有設(shè)置啟動參數(shù),則使用當(dāng)前時間有關(guān)的隨機(jī)種子算法 return (mix64(System.currentTimeMillis()) ^ mix64(System.nanoTime())); }
從上述源碼可以看出,當(dāng)我們設(shè)置了啟動參數(shù)“-Djava.util.secureRandomSeed=true”時,ThreadLocalRandom 會產(chǎn)生一個隨機(jī)種子,一定程度上能緩解隨機(jī)種子相同所帶來隨機(jī)數(shù)可預(yù)測的問題,然而默認(rèn)情況下如果不設(shè)置此參數(shù),那么在多線程中就可以因為啟動時間相同,而導(dǎo)致多個線程在每一步操作中都會生成相同的隨機(jī)數(shù)。
3.SecureRandom
SecureRandom 繼承自 Random,該類提供加密強(qiáng)隨機(jī)數(shù)生成器。SecureRandom 不同于 Random,它收集了一些隨機(jī)事件,比如鼠標(biāo)點擊,鍵盤點擊等,SecureRandom 使用這些隨機(jī)事件作為種子。這意味著,種子是不可預(yù)測的,而不像 Random 默認(rèn)使用系統(tǒng)當(dāng)前時間的毫秒數(shù)作為種子,從而避免了生成相同隨機(jī)數(shù)的可能性。
基礎(chǔ)使用
// 創(chuàng)建 SecureRandom 對象,并設(shè)置加密算法 SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); for (int i = 0; i < 10; i++) { // 生成 0-9 隨機(jī)整數(shù) int number = random.nextInt(10); // 打印結(jié)果 System.out.println("生成隨機(jī)數(shù):" + number); }
以上程序的執(zhí)行結(jié)果為:
SecureRandom 默認(rèn)支持兩種加密算法:
- SHA1PRNG 算法,提供者 sun.security.provider.SecureRandom;
- NativePRNG 算法,提供者 sun.security.provider.NativePRNG。
當(dāng)然除了上述的操作方式之外,你還可以選擇使用 new SecureRandom() 來創(chuàng)建 SecureRandom 對象,實現(xiàn)代碼如下:
SecureRandom secureRandom = new SecureRandom();
通過 new 初始化 SecureRandom,默認(rèn)會使用 NativePRNG 算法來生成隨機(jī)數(shù),但是也可以配置 JVM 啟動參數(shù)“-Djava.security”參數(shù)來修改生成隨機(jī)數(shù)的算法,或選擇使用 getInstance("算法名稱") 的方式來指定生成隨機(jī)數(shù)的算法。
4.Math
Math 類誕生于 JDK 1.0,它里面包含了用于執(zhí)行基本數(shù)學(xué)運(yùn)算的屬性和方法,如初等指數(shù)、對數(shù)、平方根和三角函數(shù),當(dāng)然它里面也包含了生成隨機(jī)數(shù)的靜態(tài)方法 Math.random() ,此方法會產(chǎn)生一個 0 到 1 的 double 值,如下代碼所示。
① 基礎(chǔ)使用
for (int i = 0; i < 10; i++) { // 產(chǎn)生隨機(jī)數(shù) double number = Math.random(); System.out.println("生成隨機(jī)數(shù):" + number); }
以上程序的執(zhí)行結(jié)果為:
② 擴(kuò)展
當(dāng)然如果你想用它來生成一個一定范圍的 int 值也是可以的,你可以這樣寫:
for (int i = 0; i < 10; i++) { // 生成一個從 0-99 的整數(shù) int number = (int) (Math.random() * 100); System.out.println("生成隨機(jī)數(shù):" + number); }
以上程序的執(zhí)行結(jié)果為:
③ 實現(xiàn)原理
通過分析 Math 的源碼我們可以得知:當(dāng)?shù)谝淮握{(diào)用 Math.random() 方法時,自動創(chuàng)建了一個偽隨機(jī)數(shù)生成器,**實際上用的是 **new java.util.Random(),當(dāng)下一次繼續(xù)調(diào)用 Math.random() 方法時,就會使用這個新的偽隨機(jī)數(shù)生成器。
源碼如下:
public static double random() { return RandomNumberGeneratorHolder.randomNumberGenerator.nextDouble(); } private static final class RandomNumberGeneratorHolder { static final Random randomNumberGenerator = new Random(); }
總結(jié)
本文我們介紹了 4 種生成隨機(jī)數(shù)的方法,其中 Math 是對 Random 的封裝,所以二者比較類似。Random 生成的是偽隨機(jī)數(shù),是以當(dāng)前納秒時間作為種子數(shù)的,并且在多線程競爭比較激烈的情況下因為要進(jìn)行 CAS 操作,所以存在一定的性能問題,但對于絕大數(shù)應(yīng)用場景來說,使用 Random 已經(jīng)足夠了。當(dāng)在競爭比較激烈的場景下可以使用 ThreadLocalRandom 來替代 Random,但如果對安全性要求比較高的情況下,可以使用 SecureRandom 來生成隨機(jī)數(shù),因為 SecureRandom 會收集一些隨機(jī)事件來作為隨機(jī)種子,所以 SecureRandom 可以看作是生成真正隨機(jī)數(shù)的一個工具類。
到此這篇關(guān)于Java中生成隨機(jī)數(shù)的4種方式與區(qū)別的文章就介紹到這了,更多相關(guān)Java生成隨機(jī)數(shù)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
參考 & 鳴謝
- Java生成N個不重復(fù)的隨機(jī)數(shù)的三種方法總結(jié)
- 如何通過Java生成一個隨機(jī)數(shù)
- Java如何生成隨機(jī)數(shù)不了解下嗎
- Java生成隨機(jī)數(shù)之Random與ThreadLocalRandom性能比較詳解
- java并發(fā)高的情況下用ThreadLocalRandom來生成隨機(jī)數(shù)
- java的三種隨機(jī)數(shù)生成方式
- Java生成的隨機(jī)數(shù)靠譜嗎?多少次會重復(fù)?
- Java實現(xiàn)生成n個不重復(fù)的隨機(jī)數(shù)
- JAVA 16位ID生成工具類含16位不重復(fù)的隨機(jī)數(shù)數(shù)字+大小寫
- java中生成任意之間數(shù)的隨機(jī)數(shù)詳解
- JavaSE API實現(xiàn)生成隨機(jī)數(shù)的2種方法(Random類和Math類的Random方法)
相關(guān)文章
Java數(shù)據(jù)結(jié)構(gòu)之二叉搜索樹詳解
二叉搜索樹作為一個經(jīng)典的數(shù)據(jù)結(jié)構(gòu),具有鏈表的快速插入與刪除的特點,同時查詢效率也很優(yōu)秀,所以應(yīng)用十分廣泛。本文將詳細(xì)講講二叉搜索樹的原理與實現(xiàn),需要的可以參考一下2022-06-06Java并發(fā)編程中的ReentrantLock類詳解
這篇文章主要介紹了Java并發(fā)編程中的ReentrantLock類詳解,ReentrantLock是juc.locks包中的一個獨占式可重入鎖,相比synchronized,它可以創(chuàng)建多個條件等待隊列,還支持公平/非公平鎖、可中斷、超時、輪詢等特性,需要的朋友可以參考下2023-12-12SpringBoot+logback默認(rèn)日志的配置和使用方式
這篇文章主要介紹了SpringBoot+logback默認(rèn)日志的配置和使用方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-05-05SpringMVC使用自定義驗證器進(jìn)行數(shù)據(jù)驗證的方法
SpringMVC?提供了強(qiáng)大的數(shù)據(jù)驗證機(jī)制,可以方便地驗證表單提交的數(shù)據(jù),除了自帶的驗證器之外,SpringMVC?還支持自定義驗證器,允許開發(fā)者根據(jù)業(yè)務(wù)需求自定義驗證規(guī)則,本文將介紹如何在?SpringMVC?中使用自定義驗證器2023-07-07基于Mybatis Plus實現(xiàn)代碼生成器CodeGenerator
這篇文章主要介紹了基于Mybatis Plus實現(xiàn)代碼生成器CodeGenerator,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-08-08