Java随机数生成研究
# 引言
# 1. 随机数生成的重要性和应用
随机数生成在多个领域有着重要应用,包括但不限于模拟、密码学和随机抽样。
在模拟中,随机数可以帮助我们创建更逼真的模型;在密码学中,随机数是保证信息安全的关键要素;在随机抽样中,随机数则帮助我们避免偏见,获取更准确的样本。
# 2. Java在随机数生成中的角色
Java作为一种广泛使用的编程语言,提供了多种随机数生成的方法。这些方法在实际应用中被广泛使用,尤其是在需要随机性的场景中。
# Java随机数生成的基础
# 1. Java中的Math.random()
在Java中,最简单的随机数生成方法可能就是使用Math.random()
了。这个方法会生成一个介于0(包含)和1(不包含)之间的双精度浮点数。
double random = Math.random();
System.out.println(random); // 输出类似 0.7172235952583826
# 2. Java的Random类
除了Math.random()
,Java还提供了一个更强大的Random
类,可以用来生成各种类型的随机数,包括int、long、float、double等。
Random random = new Random();
// 输出一个随机整数
System.out.println(random.nextInt());
// 输出一个浮点数
System.out.println(random.nextDouble());
# 3. SecureRandom类的介绍
对于需要更高安全性的应用,Java提供了SecureRandom
类。SecureRandom
生成的随机数更难以预测,因此更适合用在密码学等需要高度随机性的场景。
SecureRandom secureRandom = new SecureRandom();
int secureRandomInt = secureRandom.nextInt();
// 输出一个安全的随机整数
System.out.println(secureRandomInt);
关于SecureRandom
类的详细情况,参考:Java加密体系(JCA)
# 4. 随机数种子
随机数种子(或者简称为种子)是用于初始化随机数生成器的值。随机数生成器通常使用某种确定性算法来生成看起来像是随机的数字序列。但是,如果你用同样的种子来初始化生成器,它会生成完全相同的数字序列。因此,种子的选择会影响生成的随机数序列。
在许多情况下,种子会基于当前的时间(例如当前的毫秒或纳秒时间戳)来自动选择,以确保每次运行程序时,随机数生成器会生成不同的数字序列。然而,有时候你可能希望生成器能重复生成相同的数字序列,例如在进行模拟、测试或调试时,这时你可以手动指定一个种子。
例如,以下是一个使用种子初始化 java.util.Random
的例子:
import java.util.Random;
public class SeedExample {
public static void main(String[] args) {
// 使用种子 123 初始化 Random 对象
Random random = new Random(123);
// 输出前五个随机整数
for (int i = 0; i < 5; i++) {
System.out.println(random.nextInt());
}
}
}
在这个例子中,无论你运行多少次程序,输出的五个整数都会是相同的,因为它们都是由相同的种子生成的。
# 5. Java 17的增强伪随机数生成器
Java 17新增了JEP 356 (opens new window):增强伪随机数生成器,这个新特性引入了一套新的API和几个新的伪随机数生成器 (PRNGs)。这个API提供了一种新的方式来创建和使用伪随机数生成器,同时还引入了几种新的伪随机数生成算法。
RandomGenerator generator = RandomGenerator.of("L64X128MixRandom");
long randomNumber = generator.nextLong();
// 输出一个使用L64X128MixRandom算法生成的随机数
System.out.println(randomNumber);
Java 17提供的所有算法可以在Algorithms (opens new window)查看。
也可以使用如下代码输出所有算法:
RandomGeneratorFactory.all().forEach(factory -> {
System.out.println(factory.group() + ":" + factory.name());
});
目前主要算法有:
- L128X1024MixRandom, L128X128MixRandom, L128X256MixRandom, L32X64MixRandom, L64X1024MixRandom, L64X128MixRandom, L64X128StarStarRandom, L64X256MixRandom: 这些都是线性混合型随机数生成器(LXM),用一个较大的有限状态空间和一个线性转换在该空间中进行迭代以生成随机数。状态空间的大小由名称中的数字表示。例如,
L128X1024MixRandom
具有 1024+128 位的状态空间。 - Random: 这是 Java 提供的最早的伪随机数生成器,使用 48 位的线性同余生成器。
- SplittableRandom: 这是一个为并行计算设计的随机数生成器。它可以被“分割”成多个其他的随机数生成器,这些生成器可以在不同的线程中使用。
- ThreadLocalRandom: 这是一个为并行计算设计的随机数生成器,与
SplittableRandom
类似,但它是线程局部的,这意味着每个线程都有自己的随机数生成器。 - Xoroshiro128PlusPlus: 这是一种基于 xorshifts 和 rotations 的快速、高质量的随机数生成器,状态空间为 128 位。
- Xoshiro256PlusPlus: 这是一种基于 xorshifts 和 rotations 的快速、高质量的随机数生成器,状态空间为 256 位。
这其中有很多Java 17新增的算法(L128X1024MixRandom、Xoroshiro128PlusPlus等),他们比起Random来说具有更高的性能,更大的状态空间。
RandomGenerator
有很多子类,这些子类提供了多样的功能,让随机数生成更加的灵活。
# 基本概念
在官网看到这样一张图:
其中包含了几个关键的概念,包括周期(Period)、状态位(StateBits)和均匀分布性(Equidistribution)。这些概念用于描述和评估随机数生成器的性质和性能。以下是它们的定义:
Period(周期): 在随机数生成器中,周期是指生成器在开始重复其输出序列之前可以生成的唯一随机数的数量。换句话说,周期是生成器输出在开始重复之前的长度。一个良好的随机数生成器应该有一个非常大的周期,以确保在其应用的生命周期中不会出现重复的随机数序列。
StateBits(状态位): 在随机数生成器中,状态位是指用于存储生成器当前状态的位数。生成器的状态决定了接下来将生成的随机数。状态位的数量通常决定了生成器的周期——状态位越多,周期通常越大。
Equidistribution(均匀分布性): 均匀分布性是指随机数生成器生成的所有可能的输出在其整个周期中均匀分布的程度。理想的随机数生成器在其整个周期内都能均匀地生成所有可能的输出。在评估生成器的均匀分布性时,通常会看生成器的每个子序列(例如,连续的 k 个输出)是否都均匀分布。
可以把均匀分布性(Equidistribution)想象成一个评分系统,评价的是随机数生成器在多大程度上能公平地生成所有可能的输出。- 值为0意味着该随机数生成器的输出在全局范围内可能并不均匀,也就是说,某些数字可能会比其他数字更常出现。对于某些应用来说,这可能是可以接受的,但在需要高质量随机数的情况下,这可能是不理想的。
- 值为1意味着该随机数生成器在全局范围内的输出是均匀的,即所有可能的输出都有相同的机会被生成。这对于许多应用来说都是足够好的。
- 值为16意味着不仅单个数字的生成是均匀的,而且连续的16个数字的序列也是均匀的。这意味着这个生成器在其所有可能的16个数字的序列中生成的概率是相等的。这对于需要高度随机性的应用(如密码学)来说,可能是非常重要的。
均匀分布性的值越高,表明随机数生成器的输出越均匀。但是,"更好"的生成器是什么,取决于你的具体需求。在某些情况下,均匀分布性为1的生成器就足够好了,而在其他情况下,你可能需要一个均匀分布性更高(如16)的生成器。
这些概念是评估和选择随机数生成器的重要因素。例如,如果你需要生成非常大量的随机数,你可能需要一个具有大周期的生成器。如果你的应用依赖于生成器的输出具有良好的均匀分布性,那么你应该选择一个具有这种属性的生成器。
这里提一句BigInteger
,它用于表示任意大小的整数并进行各种数学运算。提供了各种运算,上面看到的shiftLeft
就是位移运算,左移一位相当于乘以2。
# 统计型随机数生成器
通过以下代码获得具有统计型的算法:
RandomGeneratorFactory.all().filter(RandomGeneratorFactory::isStatistical)
.forEach(e -> System.out.println(e.name()));
输出:
L32X64MixRandom
L128X128MixRandom
L64X128MixRandom
L128X1024MixRandom
L64X128StarStarRandom
Xoshiro256PlusPlus
L64X256MixRandom
Random
Xoroshiro128PlusPlus
L128X256MixRandom
SplittableRandom
L64X1024MixRandom
RandomGeneratorFactory#isStatistical
是RandomGeneratorFactory
类的一个方法,用于确定该工厂生成的随机数生成器是否是统计型的。
统计型随机数生成器是指那些为了生成高质量的随机数,使用某种形式的后处理来改善其输出的随机数生成器。这种后处理可能包括各种各样的技术,比如使用额外的混洗步骤来打乱输出的顺序,或者使用某种形式的过滤来消除输出中的某些模式或偏差。
例如,一个简单的线性同余生成器可能会在其输出中展示一些可预见的模式或偏差,因此可能不适合需要高质量随机数的应用。但是,通过添加一些额外的混洗步骤,可以改善这个生成器的输出,使其更接近于真正的随机数。
# 是否随机和是否硬件
Java 17中的RandomGeneratorFactory
类的isStochastic
和isHardware
方法用于查询该工厂生成的随机数生成器的某些特性。
isStochastic(): 这个方法返回一个布尔值,表示该工厂生成的随机数生成器是否是随机的(stochastic)。如果返回
true
,那么生成器生成的随机数序列是基于某种随机过程,这意味着不同的生成器实例,即使在相同的初始状态下,也可能产生不同的随机数序列。如果返回false
,那么生成器是确定性的(deterministic),这意味着相同的初始状态将始终产生相同的随机数序列。isHardware(): 这个方法返回一个布尔值,表示该工厂生成的随机数生成器是否是硬件的(hardware)。如果返回
true
,那么生成器利用了某种硬件设备(例如,一个随机噪声源或一个量子随机数生成器)来生成随机数。这些类型的生成器通常能够产生真正的随机数,而不仅仅是伪随机数。如果返回false
,那么生成器是软件的(software),它使用某种算法在计算机内存中生成随机数。
随机的生成器只有SecureRandom,也就是给他相同的随机种子,它也是每次生成不同序列的随机数。
Java 17默认没有提供硬件的生成器。
# StreamableGenerator
可以使用RandomGeneratorFactory
类的isStreamable
方法查询StreamableGenerator
。
StreamableGenerator
是 RandomGenerator
接口的一个子接口,它提供了生成随机数流的能力。
在许多应用中,你可能需要生成一大批随机数。例如,你可能需要在一个大的数组中填充随机数,或者你可能需要生成一个随机数序列来驱动一个模拟或蒙特卡洛方法。在这些情况下,使用 StreamableGenerator
可以使代码更简洁,更容易阅读和理解。
StreamableGenerator
接口定义了一组方法,这些方法返回一个 Java 流(Stream),其中包含指定数量的随机数。这些方法包括:
ints(long streamSize)
: 返回一个流,其中包含指定数量的随机int
值。longs(long streamSize)
: 返回一个流,其中包含指定数量的随机long
值。doubles(long streamSize)
: 返回一个流,其中包含指定数量的随机double
值。
这些方法还有一些重载版本,可以让你指定生成的随机数的范围或者其它参数。例如,ints(long streamSize, int randomNumberOrigin, int randomNumberBound)
方法返回一个流,其中包含指定数量的随机 int
值,这些值在指定的范围内。
使用 StreamableGenerator
生成的流可以与 Java 的 Stream API 一起使用,这使得处理生成的随机数变得非常灵活和强大。例如,你可以使用 filter
、map
或 reduce
操作来处理生成的随机数,或者你可以使用 collect
操作将它们收集到一个集合或数组中。
RandomGeneratorFactory<RandomGenerator> factory = RandomGeneratorFactory.all()
.filter(RandomGeneratorFactory::isStreamable)
.min((f, g) -> Integer.compare(g.stateBits(), f.stateBits())).orElseThrow();
StreamableGenerator rng = (StreamableGenerator) factory.create(1000);
rng.longs(20).parallel().forEach(System.out::println);
# JumpableGenerator
可以使用RandomGeneratorFactory
类的isJumpable
方法查询JumpableGenerator
。
JumpableGenerator
是 RandomGenerator
接口的一个子接口。这个接口定义了 jump
方法,这个方法使得生成器可以 "跳过" 它的随机数序列的一部分。
具体来说,jump
方法将生成器的状态前进到它的随机数序列中的一个 "远处" 的点,该点的位置大约相当于调用 next
方法很多次的效果(通常是 2^64 次或者更多)。这个方法返回一个新的 JumpableGenerator
对象,其状态是跳过后的状态,而原始的生成器对象的状态不变。
JumpableGenerator
的一个主要应用是在并行计算中。如果你有一个大任务需要生成大量的随机数,并且你想要将这个任务分成多个子任务并行执行,那么 JumpableGenerator
就非常有用。你可以为每个子任务创建一个新的 JumpableGenerator
,使用 jump
方法来确保每个生成器的随机数序列不会重叠。这样,每个子任务就可以独立地生成它自己的随机数,而不会影响其他子任务。
下面是一个简单的例子,展示了如何使用 JumpableGenerator
:
// 创建一个 JumpableGenerator
JumpableGenerator gen = (JumpableGenerator) RandomGeneratorFactory.of("Xoroshiro128PlusPlus").create();
// 为每个子任务创建一个跳跃后的生成器
JumpableGenerator gen1 = (JumpableGenerator) gen.copyAndJump();
// 现在 gen、gen1 都可以独立地生成随机数,而不会有重叠
int random1 = gen.nextInt();
int random2 = gen1.nextInt();
# LeapableGenerator
可以使用RandomGeneratorFactory
类的isLeapable
方法查询LeapableGenerator
。
LeapableGenerator
是 RandomGenerator
接口的另一个子接口,该接口定义了 leap
方法。类似于 JumpableGenerator
的 jump
方法,leap
方法使生成器能够 "跳过" 它的随机数序列中的一大部分。
LeapableGenerator
的主要特性是它能够进行更大范围的跳跃,通常是 2^96 次或者更多,这比 JumpableGenerator
的跳跃范围要大得多。就像 JumpableGenerator
一样,leap
方法返回一个新的 LeapableGenerator
对象,其状态是跳跃后的状态,而原始生成器的状态不变。
LeapableGenerator
在需要大量随机数,并且需要在许多并行任务之间分配这些随机数时非常有用,特别是当这些任务的数量是动态确定的,或者当任务的数量非常大(例如,超过 2^64 个)时。
下面是一个简单的例子,展示了如何使用 LeapableGenerator
:
// 创建一个 LeapableGenerator
LeapableGenerator gen = (LeapableGenerator) RandomGeneratorFactory.of("Xoroshiro128PlusPlus").create();
// 为每个子任务创建一个跳跃后的生成器
LeapableGenerator gen1 = (LeapableGenerator) gen.copyAndLeap();
// 现在 gen、gen1 都可以独立地生成随机数,而不会有重叠
int random1 = gen.nextInt();
int random2 = gen1.nextInt();
# SplittableGenerator
可以使用RandomGeneratorFactory
类的isSplittable
方法查询SplittableGenerator
。
SplittableGenerator
是 RandomGenerator
接口的一个子接口,它添加了一种新的方式来创建具有不同状态的新的随机数生成器。
SplittableGenerator
接口定义了一个 split
方法,该方法返回一个新的 SplittableGenerator
实例,其状态独立于原始生成器。这个新的生成器生成的随机数序列与原始生成器的序列不会有任何重叠。
这个特性使得 SplittableGenerator
非常适合于并行算法中,因为它可以在无需同步的情况下,为每个线程或任务提供一个独立的随机数生成器。这是因为,每当一个任务需要一个新的随机数生成器时,它只需简单地调用 split
方法即可。
以下是一个使用 SplittableGenerator
的简单示例:
// 创建一个 SplittableGenerator
SplittableGenerator gen = (SplittableGenerator) RandomGeneratorFactory.of("L32X64MixRandom").create();
// 为每个子任务创建一个分割的生成器
SplittableGenerator gen1 = gen.split();
SplittableGenerator gen2 = gen.split();
// 现在 gen、gen1 和 gen2 都可以独立地生成随机数,而不会有重叠
int random1 = gen.nextInt();
int random2 = gen1.nextInt();
int random3 = gen2.nextInt();
SplittableGenerator gen = (SplittableGenerator) RandomGeneratorFactory.of("L128X128MixRandom").create();
Stream<SplittableGenerator> splits = gen.splits(20);
splits.parallel()
.forEach(r -> System.out.println(r.nextLong()));
# ArbitrarilyJumpableGenerator
可以使用RandomGeneratorFactory
类的isArbitrarilyJumpable
方法查询ArbitrarilyJumpableGenerator
。
ArbitrarilyJumpableGenerator
是 RandomGenerator
接口的子接口,它提供了一种灵活的方式来 "跳过" 生成器的随机数序列中的任意数量的值。
具体来说,ArbitrarilyJumpableGenerator
接口定义了一个 jump(long distance)
方法。这个方法将生成器的状态前进到它的随机数序列中的一个 "远处" 的点,该点的位置大约相当于调用 next
方法 distance
次的效果。这个方法返回一个新的 ArbitrarilyJumpableGenerator
对象,其状态是跳过后的状态,而原始的生成器对象的状态不变。
这种跳跃功能的灵活性使得 ArbitrarilyJumpableGenerator
非常适合用于一些特定的并行算法,这些算法需要能够在随机数序列中任意位置跳跃。
比起JumpableGenerator
来说,它更加的灵活,可以指定跳过的多远。
# Java随机数生成的特性
# 1. 均匀性:Java随机数是否具有均匀分布
均匀性是随机数生成的一个重要特性。在理想情况下,随机数生成器生成的每个数都应该具有相等的出现概率。
在上面的内容中,我们讲到了均匀分布性,它是一个很好的参考。
# 2. 独立性:Java随机数之间的独立性
独立性是指生成的随机数之间没有关联。在大多数情况下,Java的随机数生成器可以保证生成的随机数之间的独立性。
# 3. 随机性:Java随机数生成的真随机性与伪随机性
Java中的随机数生成器实际上是伪随机数生成器,也就是说,生成的随机数并不是真正的随机,而是根据一个初始种子通过一定的算法计算出来的。虽然这些数在很大程度上看起来像是随机的,但如果知道了初始种子和算法,就可以预测到这些数。
安全的随机要使用SecureRandom,它是不可预测的。
# Java随机数在特定应用中的实践
# 1. 在模拟中的应用
在模拟中,随机数可以帮助我们创建更逼真的模型。例如,在模拟一个赌场的骰子游戏时,我们可以使用随机数来模拟骰子的结果。
Random random = new Random();
int dice = random.nextInt(6) + 1; // 模拟一个骰子的结果
System.out.println(dice); // 输出1到6之间的一个数
# 2. 在密码学中的应用
在密码学中,随机数是生成安全密钥的关键要素。我们可以使用SecureRandom
类来生成安全的随机密钥。
SecureRandom secureRandom = new SecureRandom();
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
keyGenerator.init(128, secureRandom); // 初始化一个128位的AES密钥生成器
SecretKey key = keyGenerator.generateKey(); // 生成一个安全的随机密钥
# 3. 在统计抽样中的应用
在统计抽样中,随机数可以帮助我们避免偏见,获取更准确的样本。例如,我们可以使用随机数来随机选择一个样本。
List<String> population = List.of("A", "B", "C", "D", "E");
Random random = new Random();
String sample = population.get(random.nextInt(population.size())); // 随机选择一个样本
System.out.println(sample);
# 4. JEP 356在实际应用中的表现
JEP 356提供的新的伪随机数生成器在实际应用中表现良好。例如,L64X128MixRandom
在生成大量随机数时具有更高的性能。
RandomGenerator generator = RandomGenerator.of("L64X128MixRandom");
long start = System.nanoTime();
for (int i = 0; i < 1000000; i++) {
generator.nextLong();
}
long end = System.nanoTime();
// 输出生成1000000个随机数所花费的时间
System.out.println("Time taken: " + (end - start) / 1000000.0 + " ms");
# Java随机数生成的局限性及挑战
# 1. 伪随机数的问题
尽管Java的随机数生成器可以生成看起来像随机的数,但这些数实际上是伪随机的,也就是说,它们是通过一定的算法从一个初始种子计算出来的。如果知道了初始种子和算法,就可以预测到这些数。这在需要高度随机性的应用中可能会成为一个问题。
# 2. 安全随机数生成的挑战
虽然SecureRandom
类可以生成更难以预测的随机数,但它的性能通常比Random
类要低。这在需要大量生成随机数的情况下可能会成为一个问题。
# 3. 随机数生成速度与随机性的权衡
在随机数生成中,速度和随机性往往是一对矛盾。提高随机性通常需要更复杂的算法,而这可能会降低生成速度。反之,提高生成速度可能会牺牲随机性。
# 总结
Java在随机数生成方面提供了多种工具和方法,满足了不同场景的需求。
特别是Java 17提供了增强伪随机数生成器,带来了更好的性能和均匀分布性。
期待未来Java提供更多的算法,以进一步提高Java在随机数生成方面的能力。
祝你变得更强!