Java随机数生成研究
# 引言
# 1. 随机数生成的重要性和应用
随机数生成在多个领域有着重要应用,包括但不限于模拟、密码学和随机抽样。
在模拟中,随机数可以帮助我们创建更逼真的模型;在密码学中,随机数是保证信息安全的关键要素;在随机抽样中,随机数则帮助我们避免偏见,获取更准确的样本。
其他重要应用场景包括:
- 游戏开发:生成随机地图、物品掉落、AI行为决策
- 机器学习:权重初始化、数据增强、随机森林等算法
- 分布式系统:负载均衡、一致性哈希、分片策略
- 测试:模糊测试、随机测试用例生成、性能测试数据
# 2. Java在随机数生成中的角色
Java作为一种广泛使用的编程语言,提供了多种随机数生成的方法。从简单的Math.random()
到复杂的SecureRandom
,再到Java 17引入的增强伪随机数生成器,Java的随机数生成能力不断演进。
# Java随机数生成的基础
# 1. Java中的Math.random()
在Java中,最简单的随机数生成方法可能就是使用Math.random()
了。这个方法会生成一个介于0(包含)和1(不包含)之间的双精度浮点数。
double random = Math.random();
System.out.println(random); // 输出类似 0.7172235952583826
// 生成指定范围的随机整数
int min = 10;
int max = 50;
int randomInt = (int)(Math.random() * (max - min + 1)) + min;
System.out.println(randomInt); // 输出10到50之间的随机整数
// 生成随机布尔值
boolean randomBoolean = Math.random() < 0.5;
System.out.println(randomBoolean); // 随机输出true或false
Math.random()
内部实际上使用了Random
类的单例实例,因此在多线程环境下可能存在竞争。
# 2. Java的Random类
除了Math.random()
,Java还提供了一个更强大的Random
类,可以用来生成各种类型的随机数,包括int、long、float、double等。
Random random = new Random();
// 生成不同类型的随机数
System.out.println(random.nextInt()); // 任意整数
System.out.println(random.nextInt(100)); // 0-99之间的整数
System.out.println(random.nextDouble()); // 0.0-1.0之间的double
System.out.println(random.nextBoolean()); // 随机布尔值
System.out.println(random.nextGaussian()); // 高斯分布的随机数
// 生成随机字节数组
byte[] bytes = new byte[10];
random.nextBytes(bytes);
// 使用Stream API生成随机数流
random.ints(5, 0, 100) // 生成5个0-99之间的随机整数
.forEach(System.out::println);
// 线程安全的替代方案
Random threadSafeRandom = ThreadLocalRandom.current();
int randomNum = threadSafeRandom.nextInt(1, 101); // 1-100之间
Random
类使用线性同余生成器(LCG)算法,其内部维护了一个48位的种子。
# 3. SecureRandom类的介绍
对于需要更高安全性的应用,Java提供了SecureRandom
类。SecureRandom
生成的随机数更难以预测,因此更适合用在密码学等需要高度随机性的场景。
// 使用默认算法
SecureRandom secureRandom = new SecureRandom();
int secureRandomInt = secureRandom.nextInt();
System.out.println(secureRandomInt);
// 指定特定算法
try {
SecureRandom sha1Random = SecureRandom.getInstance("SHA1PRNG");
SecureRandom nativeRandom = SecureRandom.getInstance("NativePRNG");
// 生成安全的随机令牌
byte[] token = new byte[32];
secureRandom.nextBytes(token);
String tokenHex = bytesToHex(token);
System.out.println("Token: " + tokenHex);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
// 辅助方法:字节数组转十六进制
private static String bytesToHex(byte[] bytes) {
StringBuilder result = new StringBuilder();
for (byte b : bytes) {
result.append(String.format("%02x", b));
}
return result.toString();
}
SecureRandom
使用操作系统的熵源(如/dev/random
或/dev/urandom
),生成真正的随机数,但性能较Random
慢。
关于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,它是不可预测的。
# 性能比较
# 各种随机数生成器的性能测试
public class RandomPerformanceTest {
private static final int ITERATIONS = 10_000_000;
public static void main(String[] args) {
// 测试Math.random()
long start = System.nanoTime();
for (int i = 0; i < ITERATIONS; i++) {
Math.random();
}
long mathTime = System.nanoTime() - start;
// 测试Random
Random random = new Random();
start = System.nanoTime();
for (int i = 0; i < ITERATIONS; i++) {
random.nextDouble();
}
long randomTime = System.nanoTime() - start;
// 测试ThreadLocalRandom
start = System.nanoTime();
for (int i = 0; i < ITERATIONS; i++) {
ThreadLocalRandom.current().nextDouble();
}
long threadLocalTime = System.nanoTime() - start;
// 测试SecureRandom
SecureRandom secureRandom = new SecureRandom();
start = System.nanoTime();
for (int i = 0; i < ITERATIONS / 100; i++) { // 减少迭代次数
secureRandom.nextDouble();
}
long secureTime = (System.nanoTime() - start) * 100;
// Java 17的新生成器
RandomGenerator l64 = RandomGenerator.of("L64X128MixRandom");
start = System.nanoTime();
for (int i = 0; i < ITERATIONS; i++) {
l64.nextDouble();
}
long l64Time = System.nanoTime() - start;
// 输出结果
System.out.println("Performance Results (ms):");
System.out.println("Math.random(): " + mathTime / 1_000_000);
System.out.println("Random: " + randomTime / 1_000_000);
System.out.println("ThreadLocalRandom: " + threadLocalTime / 1_000_000);
System.out.println("SecureRandom: " + secureTime / 1_000_000);
System.out.println("L64X128MixRandom: " + l64Time / 1_000_000);
}
}
典型输出(结果会因硬件而异):
Performance Results (ms):
Math.random(): 125
Random: 89
ThreadLocalRandom: 45
SecureRandom: 2850
L64X128MixRandom: 67
# Java随机数在特定应用中的实践
# 1. 在模拟中的应用
在模拟中,随机数可以帮助我们创建更逼真的模型。例如,在模拟一个赌场的骰子游戏时,我们可以使用随机数来模拟骰子的结果。
public class CasinoSimulation {
private final Random random = new Random();
// 模拟掷骰子
public int rollDice() {
return random.nextInt(6) + 1;
}
// 模拟21点游戏发牌
public int drawCard() {
int card = random.nextInt(13) + 1;
return Math.min(card, 10); // J、Q、K都算10点
}
// 蒙特卡洛方法估算π
public double estimatePi(int iterations) {
int insideCircle = 0;
for (int i = 0; i < iterations; i++) {
double x = random.nextDouble();
double y = random.nextDouble();
if (x * x + y * y <= 1) {
insideCircle++;
}
}
return 4.0 * insideCircle / iterations;
}
}
# 2. 在密码学中的应用
在密码学中,随机数是生成安全密钥的关键要素。我们可以使用SecureRandom
类来生成安全的随机密钥。
public class CryptoUtils {
private static final SecureRandom secureRandom = new SecureRandom();
// 生成AES密钥
public static SecretKey generateAESKey(int keySize) throws Exception {
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(keySize, secureRandom);
return keyGen.generateKey();
}
// 生成安全的盐值
public static byte[] generateSalt(int length) {
byte[] salt = new byte[length];
secureRandom.nextBytes(salt);
return salt;
}
// 生成UUID风格的令牌
public static String generateSecureToken() {
byte[] randomBytes = new byte[16];
secureRandom.nextBytes(randomBytes);
return UUID.nameUUIDFromBytes(randomBytes).toString();
}
// 生成指定长度的随机密码
public static String generatePassword(int length) {
String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*";
StringBuilder password = new StringBuilder(length);
for (int i = 0; i < length; i++) {
password.append(chars.charAt(secureRandom.nextInt(chars.length())));
}
return password.toString();
}
// 生成RSA密钥对
public static KeyPair generateRSAKeyPair(int keySize) throws Exception {
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");
keyPairGen.initialize(keySize, secureRandom);
return keyPairGen.generateKeyPair();
}
}
# 3. 在统计抽样中的应用
在统计抽样中,随机数可以帮助我们避免偏见,获取更准确的样本。
public class SamplingUtils {
private final Random random = new Random();
// 简单随机抽样
public <T> T randomSample(List<T> population) {
if (population.isEmpty()) return null;
return population.get(random.nextInt(population.size()));
}
// 不放回抽样(洗牌算法)
public <T> List<T> randomSampleWithoutReplacement(List<T> population, int sampleSize) {
List<T> copy = new ArrayList<>(population);
Collections.shuffle(copy, random);
return copy.subList(0, Math.min(sampleSize, copy.size()));
}
// 水塘抽样算法(适用于流式数据)
public <T> List<T> reservoirSampling(Stream<T> stream, int k) {
List<T> reservoir = new ArrayList<>(k);
AtomicInteger count = new AtomicInteger(0);
stream.forEach(item -> {
int index = count.getAndIncrement();
if (index < k) {
reservoir.add(item);
} else {
int randomIndex = random.nextInt(index + 1);
if (randomIndex < k) {
reservoir.set(randomIndex, item);
}
}
});
return reservoir;
}
// 加权随机选择
public <T> T weightedRandomChoice(Map<T, Double> weights) {
double totalWeight = weights.values().stream().mapToDouble(Double::doubleValue).sum();
double randomValue = random.nextDouble() * totalWeight;
double cumulativeWeight = 0.0;
for (Map.Entry<T, Double> entry : weights.entrySet()) {
cumulativeWeight += entry.getValue();
if (randomValue <= cumulativeWeight) {
return entry.getKey();
}
}
return null;
}
}
# 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");
# 最佳实践与常见陷阱
# 1. 最佳实践
public class RandomBestPractices {
// ✅ 推荐:重用Random实例
private static final Random RANDOM = new Random();
// ✅ 推荐:多线程环境使用ThreadLocalRandom
public void multithreadedExample() {
IntStream.range(0, 1000)
.parallel()
.forEach(i -> {
int value = ThreadLocalRandom.current().nextInt(100);
// 处理随机数
});
}
// ✅ 推荐:密码学应用使用SecureRandom
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
// ❌ 避免:每次都创建新的Random实例
public void badPractice() {
for (int i = 0; i < 1000; i++) {
Random random = new Random(); // 性能差,可能产生相同序列
int value = random.nextInt();
}
}
// ✅ 推荐:正确处理范围
public int getRandomInRange(int min, int max) {
if (min >= max) {
throw new IllegalArgumentException("max must be greater than min");
}
return RANDOM.nextInt(max - min) + min;
}
// ✅ 推荐:使用合适的生成器
public void chooseRightGenerator() {
// 游戏或模拟:使用Random或ThreadLocalRandom
Random gameRandom = new Random();
// 密码学:使用SecureRandom
SecureRandom cryptoRandom = new SecureRandom();
// 高性能需求:使用Java 17的新生成器
RandomGenerator fastRandom = RandomGenerator.of("L64X128MixRandom");
// 并行计算:使用SplittableGenerator
SplittableGenerator parallelRandom =
(SplittableGenerator) RandomGeneratorFactory.of("L128X256MixRandom").create();
}
}
# 2. 常见陷阱与解决方案
public class RandomPitfalls {
// 陷阱1:种子相同导致序列相同
public void seedProblem() {
// ❌ 错误:使用固定种子
Random r1 = new Random(42);
Random r2 = new Random(42);
// r1和r2会产生相同的序列
// ✅ 正确:使用不同的种子或默认构造器
Random r3 = new Random();
Random r4 = new Random();
}
// 陷阱2:nextInt(n)的参数错误
public void rangeProblem() {
Random random = new Random();
// ❌ 错误:想要1-10,结果得到0-9
int wrong = random.nextInt(10);
// ✅ 正确:1-10的范围
int correct = random.nextInt(10) + 1;
}
// 陷阱3:线程安全问题
private Random sharedRandom = new Random();
public void threadSafetyProblem() {
// ❌ 错误:多线程共享Random实例
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
executor.submit(() -> {
int value = sharedRandom.nextInt(); // 性能瓶颈
});
}
// ✅ 正确:使用ThreadLocalRandom
for (int i = 0; i < 1000; i++) {
executor.submit(() -> {
int value = ThreadLocalRandom.current().nextInt();
});
}
}
// 陷阱4:浮点数精度问题
public void floatingPointProblem() {
Random random = new Random();
// ❌ 可能的精度问题
double value = random.nextDouble() * 100;
// ✅ 使用BigDecimal进行精确计算
BigDecimal precise = new BigDecimal(random.nextDouble())
.multiply(new BigDecimal("100"))
.setScale(2, RoundingMode.HALF_UP);
}
}
# Java随机数生成的局限性及挑战
# 1. 伪随机数的问题
尽管Java的随机数生成器可以生成看起来像随机的数,但这些数实际上是伪随机的,也就是说,它们是通过一定的算法从一个初始种子计算出来的。如果知道了初始种子和算法,就可以预测到这些数。这在需要高度随机性的应用中可能会成为一个问题。
# 2. 安全随机数生成的挑战
虽然SecureRandom
类可以生成更难以预测的随机数,但它的性能通常比Random
类要低。这在需要大量生成随机数的情况下可能会成为一个问题。
# 3. 随机数生成速度与随机性的权衡
在随机数生成中,速度和随机性往往是一对矛盾。提高随机性通常需要更复杂的算法,而这可能会降低生成速度。反之,提高生成速度可能会牺牲随机性。
# 实际应用案例
# 1. 分布式ID生成器
public class DistributedIdGenerator {
private final SecureRandom random = new SecureRandom();
private final int nodeId;
private long lastTimestamp = -1L;
private long sequence = 0L;
public DistributedIdGenerator(int nodeId) {
this.nodeId = nodeId;
}
// 雪花算法变种,结合随机数
public synchronized long generateId() {
long timestamp = System.currentTimeMillis();
if (timestamp == lastTimestamp) {
sequence = (sequence + 1) & 0xFFF;
if (sequence == 0) {
// 序列号用尽,等待下一毫秒
timestamp = waitNextMillis(lastTimestamp);
}
} else {
// 新的毫秒,使用随机序列号起始值
sequence = random.nextInt(100);
}
lastTimestamp = timestamp;
return ((timestamp - 1640995200000L) << 22) |
(nodeId << 12) |
sequence;
}
private long waitNextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
}
# 2. A/B测试分流器
public class ABTestSplitter {
private final Random random = new Random();
// 简单的百分比分流
public String assignGroup(String userId, double testGroupPercentage) {
// 使用用户ID作为种子,保证同一用户总是分到同一组
Random userRandom = new Random(userId.hashCode());
return userRandom.nextDouble() < testGroupPercentage ? "test" : "control";
}
// 多组分流
public String assignMultipleGroups(String userId, Map<String, Double> groupWeights) {
Random userRandom = new Random(userId.hashCode());
double randomValue = userRandom.nextDouble();
double cumulative = 0.0;
for (Map.Entry<String, Double> entry : groupWeights.entrySet()) {
cumulative += entry.getValue();
if (randomValue < cumulative) {
return entry.getKey();
}
}
return "default";
}
// 时间窗口内的一致性分流
public String consistentAssignment(String userId, long windowStart, double percentage) {
// 结合用户ID和时间窗口
long seed = (userId.hashCode() * 31L) + (windowStart / 86400000L);
Random consistentRandom = new Random(seed);
return consistentRandom.nextDouble() < percentage ? "test" : "control";
}
}
# 3. 负载均衡器
public class LoadBalancer {
private final ThreadLocalRandom random = ThreadLocalRandom.current();
private final List<String> servers;
private final Map<String, Integer> serverWeights;
public LoadBalancer(List<String> servers, Map<String, Integer> weights) {
this.servers = new ArrayList<>(servers);
this.serverWeights = new HashMap<>(weights);
}
// 随机负载均衡
public String randomSelect() {
return servers.get(random.nextInt(servers.size()));
}
// 加权随机负载均衡
public String weightedRandomSelect() {
int totalWeight = serverWeights.values().stream().mapToInt(Integer::intValue).sum();
int randomWeight = random.nextInt(totalWeight);
int currentWeight = 0;
for (Map.Entry<String, Integer> entry : serverWeights.entrySet()) {
currentWeight += entry.getValue();
if (randomWeight < currentWeight) {
return entry.getKey();
}
}
return servers.get(0);
}
// 随机两选一(P2C)算法
public String powerOfTwoChoices(Map<String, Integer> serverLoads) {
int idx1 = random.nextInt(servers.size());
int idx2 = random.nextInt(servers.size());
String server1 = servers.get(idx1);
String server2 = servers.get(idx2);
int load1 = serverLoads.getOrDefault(server1, 0);
int load2 = serverLoads.getOrDefault(server2, 0);
return load1 <= load2 ? server1 : server2;
}
}
# 4. 游戏物品掉落系统
public class LootSystem {
private final Random random = new Random();
static class LootItem {
String name;
double dropRate;
int minQuantity;
int maxQuantity;
public LootItem(String name, double dropRate, int min, int max) {
this.name = name;
this.dropRate = dropRate;
this.minQuantity = min;
this.maxQuantity = max;
}
}
// 单个物品掉落判定
public boolean shouldDrop(double dropRate, double luckModifier) {
double adjustedRate = Math.min(1.0, dropRate * (1 + luckModifier));
return random.nextDouble() < adjustedRate;
}
// 掉落表系统
public List<Map.Entry<String, Integer>> generateLoot(
List<LootItem> lootTable,
double luckModifier) {
List<Map.Entry<String, Integer>> drops = new ArrayList<>();
for (LootItem item : lootTable) {
if (shouldDrop(item.dropRate, luckModifier)) {
int quantity = item.minQuantity +
random.nextInt(item.maxQuantity - item.minQuantity + 1);
drops.add(Map.entry(item.name, quantity));
}
}
return drops;
}
// 保底机制
public boolean dropWithPity(double baseRate, int attempts) {
// 每次失败增加概率
double adjustedRate = Math.min(1.0, baseRate * (1 + attempts * 0.1));
return random.nextDouble() < adjustedRate;
}
}
# 总结
# 选择指南
场景 | 推荐方案 | 原因 |
---|---|---|
简单随机数 | Math.random() | 最简单,适合非关键场景 |
一般应用 | Random | 功能完整,性能适中 |
多线程应用 | ThreadLocalRandom | 避免线程竞争,性能更好 |
密码学应用 | SecureRandom | 真随机性,安全性高 |
高性能需求 | Java 17新生成器 | 性能优秀,状态空间大 |
并行计算 | SplittableGenerator | 支持分割,适合并行 |
流式处理 | StreamableGenerator | 与Stream API完美结合 |
# 关键要点
- 理解伪随机与真随机的区别:大多数场景下伪随机已经足够,只有密码学等安全场景才需要真随机
- 选择合适的生成器:不同生成器有不同的性能和特性,根据实际需求选择
- 注意线程安全:在多线程环境下优先使用
ThreadLocalRandom
- 正确使用种子:理解种子的作用,在需要可重现性时使用固定种子,其他情况使用默认种子
- 性能与安全的权衡:
SecureRandom
虽然安全但性能较差,需要根据场景权衡
# 发展趋势
随着Java版本的迭代,随机数生成器也在不断改进:
- 更好的算法:Java 17引入的LXM系列算法提供了更大的状态空间和更好的统计特性
- 更灵活的API:新的
RandomGenerator
接口体系提供了更灵活的扩展性 - 更好的并行支持:
SplittableGenerator
等接口为并行计算提供了更好的支持 - 硬件支持:未来可能会引入硬件随机数生成器的支持
期待未来Java在随机数生成方面继续改进,提供更多高质量的算法和更便捷的API。
祝你变得更强!