我們一起聊聊 Java 隨機數的種子
在許多領域,比如模擬、游戲和密碼學中,隨機數擔任非常重要的角色。
然而,在計算機領域,隨機數并非完全隨機,它們是由模擬隨機性的算法(稱為偽隨機性)生成的。
在Java中,隨機種子就是初始化偽隨機數生成器(PRNG,Pseudo Random Number Generator)的值。
我們一起探討下,Java中隨機種子的工作原理,以及如何使用它生成可預測的數字序列。
一、什么是隨機種子?
隨機種子是設置PRNG(偽隨機數生成器)內部狀態的初始值。
默認情況下,如果我們指定種子值,Java的Random類會使用系統時鐘作為種子值。這樣做的好處是,確保了每次創建新的Random對象時,生成的數字序列都是不同的,增加了隨機性。
如果我們提供特定的種子值,每次都會生成相同的“隨機”數字序列。這在我們需要可重復性的情況下非常有用,比如測試、調試或需要結果一致性的模擬場景。
有了種子值之后,PRNG算法會基于種子值生成一系列數字。
每次我們調用nextInt()、nextDouble()或類似方法時,它都會更新生成器的內部狀態,從而保證每次生成一個新數字。但是,如果使用相同的種子,生成的數字序列將始終相同。
接下來我們看下這兩種情況。
二、不使用種子生成隨機數
Java提供了java.util.Random類,用于生成隨機數。
當我們創建一個Random實例而不指定種子時,Java會使用系統時鐘為生成器設定種子。這意味著每次運行都會產生不同的序列。例如:
import java.util.Random;
public class RandomWithoutSeed {
public static void main(String[] args) {
Random random = new Random();
// 生成7個隨機整數
for (int i = 0; i < 7; i++) {
System.out.format("%d \t", random.nextInt(100)); // 0到99之間的隨機整數
}
System.out.println();
Random random2 = new Random();
for (int i = 0; i < 7; i++) {
System.out.format("%d \t", random2.nextInt(100)); // 0到99之間的隨機整數
}
System.out.println();
Random random3 = new Random();
for (int i = 0; i < 7; i++) {
System.out.format("%d \t", random3.nextInt(100)); // 0到99之間的隨機整數
}
}
}
在這個例子中,每次運行都會生成不同的隨機整數序列,因為種子是根據當前時間自動設置的。
第一次運行結果是:
76 9 11 77 67 91 91
76 44 28 5 91 59 30
41 18 72 14 6 4 63
在運行一次:
33 65 97 31 94 19 1
97 2 40 58 9 33 57
46 82 21 94 54 36 79
可以看出來,結果基本上符合隨機性。(上面的結果只是展示下隨機效果,每次運行都會有差異)
三、使用種子生成隨機數
當我們提供特定的種子時,生成的數字序列在不同的運行中是可預測且一致的。
import java.util.Random;
public class RandomWithSeed {
public static void main(String[] args) {
Random random = new Random(12345L); // 種子設置為12345
// 生成7個隨機整數
for (int i = 0; i < 7; i++) {
System.out.format("%d \t", random.nextInt(100)); // 0到99之間的隨機整數
}
System.out.println();
Random random2 = new Random(12345L); // 種子設置為12345
for (int i = 0; i < 7; i++) {
System.out.format("%d \t", random2.nextInt(100)); // 0到99之間的隨機整數
}
System.out.println();
Random random3 = new Random(12345L); // 種子設置為12345
for (int i = 0; i < 7; i++) {
System.out.format("%d \t", random3.nextInt(100)); // 0到99之間的隨機整數
}
}
}
在這里,Random類的構造函數接受一個種子值作為參數,在這個例子中,種子被設置為12345L(一個特定的長整型值)。
這個種子初始化偽隨機數生成器(PRNG),重要的是,它確保如果程序使用相同的種子運行,將始終生成相同的數字序列。
第一次運行結果是:
51 80 41 28 55 84 75
51 80 41 28 55 84 75
51 80 41 28 55 84 75
再來一次還是這樣:
51 80 41 28 55 84 75
51 80 41 28 55 84 75
51 80 41 28 55 84 75
所以說,“隨機”是可以操縱的。
四、使用SecureRandom
在密碼學應用中,使用可預測的隨機數可能會導致安全漏洞。
Java提供了SecureRandom類用于生成密碼學安全的隨機數。
看名字就知道,SecureRandom安全等級高一些。
import java.security.SecureRandom;
public class SecureRandomExample {
public static void main(String[] args) throws Exception {
SecureRandom random = new SecureRandom(new byte[] {1, 2, 3, 4, 5});
// 生成7個隨機整數
for (int i = 0; i < 7; i++) {
System.out.format("%d \t", random.nextInt(100)); // 0到99之間的隨機整數
}
System.out.println();
SecureRandom random2 = new SecureRandom(new byte[] {1, 2, 3, 4, 5});
// 生成7個隨機整數
for (int i = 0; i < 7; i++) {
System.out.format("%d \t", random2.nextInt(100)); // 0到99之間的隨機整數
}
System.out.println();
SecureRandom random3 = new SecureRandom(new byte[] {1, 2, 3, 4, 5});
// 生成7個隨機整數
for (int i = 0; i < 7; i++) {
System.out.format("%d \t", random3.nextInt(100)); // 0到99之間的隨機整數
}
}
}
上面的例子中,我們傳入相同的種子,運行結果也是隨機的。
第一次運行:
78 68 56 24 73 13 88
24 14 20 69 25 4 61
25 8 32 39 25 16 87
第二次運行:
4 35 46 26 48 92 66
83 92 28 64 13 75 44
60 79 81 52 7 66 11
結果也是足夠隨機的。(上面的結果只是展示下隨機效果,每次運行都會有差異)
SecureRandom使用高熵值的源來初始化其內部狀態。熵是對不確定性或隨機性的度量,高熵源意味著具有更多的隨機性。常見的熵源包括:
- 操作系統提供的隨機數據:許多操作系統都有內置的隨機數生成器,它們從硬件設備(如鼠標移動、鍵盤敲擊時間間隔、磁盤 I/O 操作等)收集隨機事件產生的數據,這些數據具有較高的隨機性,SecureRandom可以從中獲取種子或隨機數據來初始化自身。
- 硬件隨機數生成器:某些計算機系統配備了專門的硬件設備來生成真正的隨機數,例如基于熱噪聲、放射性衰變等物理現象的硬件隨機數生成器。這些硬件設備能夠產生高質量的隨機數,SecureRandom可以直接使用或結合這些硬件生成的隨機數來增強隨機性。
SecureRandom會維護一個內部狀態,該狀態在每次生成隨機數時都會更新。新生成的隨機數不僅取決于當前的熵源數據,還與之前的內部狀態有關。這種狀態更新機制使得生成的隨機數序列更加難以預測,即使攻擊者獲取了部分隨機數,也難以推斷出后續的隨機數。
與普通的Random類不同,SecureRandom對種子的管理更為嚴格。它可以自動從可靠的熵源獲取種子,以確保每次初始化時都有足夠的隨機性。
雖然允許用戶提供種子,但通常建議讓系統自動管理種子,以充分利用高質量的熵源。
需要注意的是,假設在一個非??臻e的機器上,SecureRandom使用高熵值可能會使服務卡死,機器沒有足夠的隨機信息,SecureRandom無法生成種子,就難以運行了。