在许多领域,比如模拟、游戏和密码学中,随机数担任非常重要的角色。
然而,在计算机领域,随机数并非完全随机,它们是由模拟随机性的算法(称为伪随机性)生成的。
在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对种子的管理更为严格。它可以自动从可靠的熵源获取种子,以确保每次初始化时都有足够的随机性。
虽然允许用户提供种子,但通常建议让系统自动管理种子,以充分利用高质量的熵源。
需要注意的是,假设在一个非常空闲的机器上,SecureRandom使用高熵值可能会使服务卡死,机器没有足够的随机信息,SecureRandom无法生成种子,就难以运行了。