介紹
世界上有許多科學研究機率論。科學由不同的部分組成。例如,在數學中,有一個單獨的部分專門研究隨機事件、數量等。但科學並沒有被輕視。在這種情況下,當人們試圖了解玩機會遊戲時擲骰子的模式時,機率論就開始成形。如果你仔細觀察,我們周圍有很多看似隨機的事情。但一切隨機的事情並不完全隨機。但稍後會詳細介紹。從 JDK 的第一個版本開始,Java 程式語言也支援隨機數。
Java 中的隨機數可以透過java.util.Random類別來使用。為了進行測試,我們將使用
tutorialspoint java線上編譯器。
以下是使用Random來模擬投擲「骰子」(俄語中的立方體) 的原始範例:
import java.util.Random;
public class HelloWorld{
public static void main(String []args){
Random rnd = new Random();
int number = rnd.nextInt(6) + 1;
System.out.println("Random number: " + number);
}
}
看起來對
Random的描述到此就結束了,但事實並非如此簡單。我們打開Java API中
java.util.Random類別的描述。在這裡我們看到了有趣的事。
Random類別使用偽隨機數。為何如此?原來隨機數並沒有那麼隨機?
偽隨機性 java.util.Random
java.util.Random類別的文件表示,如果使用相同的
種子參數建立
Random實例並對實例執行相同的操作序列,則它們將傳回相同的數字序列。如果我們仔細觀察,我們可以看到
Random實際上有
一個構造函數,它以一些
long值作為
種子:
Random rnd1 = new Random(1L);
Random rnd2 = new Random(1L);
boolean test = rnd1.nextInt(6) == rnd2.nextInt(6);
System.out.println("Test: " + test);
這個例子將會回傳
true因為
兩個實例的種子是相同的。怎麼辦?預設構造函數部分解決了這個問題。
以下是Random建構函數的內容範例:
public Random() {
this(seedUniquifier() ^ System.nanoTime());
}
預設構造函數使用位元異
或運算。並使用代表當前時間的
long和一些種子:
private static long seedUniquifier() {
for (;;) {
long current = seedUniquifier.get();
long next = current * 181783497276652981L;
if (seedUniquifier.compareAndSet(current, next))
return next;
}
}
這裡另一個有趣的事情是,每次呼叫
SeedUniquifier getter 方法都會更改SeedUniquifier 的值。也就是說,該類別被設計為盡可能有效地選擇隨機數。然而,正如文件所說,它們「
在加密上並不安全」。也就是說,對於某些用於加密目的(密碼產生等)的用途來說,它是不適合的,因為 使用正確方法預測序列。網路上有關於此主題的範例,例如:「
Predicting the next Math.random() in Java」。或例如這裡的源代碼:“
Vulnerability Weak Crypto ”。java.util.Random (隨機數產生器)有一個特定的“捷徑”,
即透過 Math.random 執行的呼叫的縮短版本:
public static void main(String []args){
int random_number = 1 + (int) (Math.random() * 6);
System.out.println("Value: " + random_number);
}
但仔細觀察,你會發現裡面有相同的隨機數字:
public static double random() {
return RandomNumberGeneratorHolder.randomNumberGenerator.nextDouble();
}
private static final class RandomNumberGeneratorHolder {
static final Random randomNumberGenerator = new Random();
}
JavaDoc 建議使用
SecureRandom類別作為「
加密安全偽隨機數產生器」。
安全隨機 Java
SecureRandom類是
java.util.Random的子類,位於
java.security套件中。這兩個類別的比較可以閱讀文章「
java.util.Random 和 java.security.SecureRandom 之間的差異」。為什麼這個 SecureRandom 這麼好?事實上,對他來說,隨機數的來源就是「核心熵池」這樣一個聽起來很神奇的東西。這既是優點也是缺點。您可以在文章“
java.security.SecureRandom 的危險”中了解其缺點。簡而言之,Linux 有一個核心隨機數產生器(RNG)。RNG 根據熵池中的資料產生隨機數,熵池根據系統中的隨機事件填充,例如鍵盤和磁碟計時、滑鼠移動、中斷和網路流量。有關熵池的更多信息,請參閱材料“
Linux 中的隨機數 (RNG) 或如何“填充”/dev/random 和 /dev/urandom ”。在 Windows 系統上,使用 SHA1PRNG,在 sun.security.provider.SecureRandom 中實作。隨著 Java 的發展,SecureRandom 也發生了變化,值得閱讀評論「
截至 2016 年 4 月的 Java SecureRandom 更新」以了解完整情況。
多線程或者像凱撒一樣
如果您查看
Random類的程式碼,似乎沒有任何跡象表明有問題。方法未標記
為同步。但有一個 BUT:當在多個執行緒中使用預設建構函式建立
Random時,我們將在它們之間共享相同的實例種子,透過該種子來建立
Random。而當收到新的隨機數時,
實例的內部AtomicLong也會改變。一方面,從邏輯的角度來看,這並沒有什麼問題,因為… 使用
AtomicLong。另一方面,你必須為一切付出代價,包括生產力。也是為了這個。
因此,即使java.util.Random的官方文件也說:「
java.util.Random 的實例是線程安全的。但是,跨線程並發使用同一個java.util.Random 實例可能會遇到爭用,從而導致效能不佳。考慮相反,在多線程設計中使用 ThreadLocalRandom」。也就是說,在多執行緒應用程式中,當從多個執行緒主動使用
Random時,最好使用
ThreadLocalRandom類別。
它的用法與常規Random略有不同:
public static void main(String []args){
int rand = ThreadLocalRandom.current().nextInt(1,7);
System.out.println("Value: " + rand);
}
正如您所看到的,我們沒有為其指定
種子。Oracle 的官方教學:
Concurrent Random Numbers中描述了此範例。您可以在評論中閱讀有關此類的更多資訊:「
Guide to ThreadLocalRandom in Java」。
StreamAPI 和隨機
隨著 Java 8 的發布,我們有了許多新功能。包括流API。
這些變化也影響了隨機值的產生。
例如, Random類別具有新方法,可讓您取得具有隨機值(如、或 )的
Stream。例如:
int
double
long
import java.util.Random;
public class HelloWorld{
public static void main(String []args){
new Random().ints(10, 1, 7).forEach(n -> System.out.println(n));
}
}
還有一個新類別
SplittableRandom:
import java.util.SplittableRandom;
public class HelloWorld{
public static void main(String []args){
new SplittableRandom().ints(10, 1, 7).forEach(n -> System.out.println(n));
}
}
您可以在此處閱讀有關SplittableRandom和其他類別 之間的區別的更多資訊:「
在 Java 中創建隨機數的不同方法」。
結論
我認為值得下結論。您需要仔細閱讀所使用的類別的 JavaDoc。像是「隨機」乍看之下很簡單的東西背後,隱藏著可能會開一個殘酷玩笑的細微差別。#維亞切斯拉夫
GO TO FULL VERSION