Contents

Java中的随机数生成器17

1. 概述

Java SE 17的发布引入了用于生成随机数的 API 的更新——JEP 356 。 通过此 API 更新,引入了新的接口类型,以及轻松列出、查找和实例化生成器工厂的方法。此外,现在可以使用一组新的随机数生成器实现。 在本教程中,我们将比较新的RandomGenerator API 和旧的Random API。我们将查看列出所有可用的生成器工厂并根据其名称或属性选择生成器。 我们还将探索新 API 的线程安全性和性能。

2. 旧的随机 API

首先,让我们看一下基于Random类生成随机数的 Java 旧 API。

2.1. API 设计

原始 API 由四个没有接口的类组成: /uploads/java_17_random_number_generators/1.png

2.2. 随机的

最常用的随机数生成器是java.util包中的Random 。 要生成随机数流,我们需要创建一个随机数生成器类的实例——Random

Random random = new Random();
int number = random.nextInt(10);
assertThat(number).isPositive().isLessThan(10);

在这里,默认构造函数将随机数生成器的种子设置为一个很可能不同于任何其他调用的值。

2.3. 备择方案

除了java.util.Random之外,还可以使用三个替代生成器来解决线程安全和安全问题

默认情况下,所有Random实例都是线程安全的。但是,跨线程并发使用同一实例可能会导致性能下降。因此,来自java.util.concurrent 包的ThreadLocalRandom类是多线程系统的首选。 由于 Random实例在密码学上不安全,因此SecureRandom类使我们能够创建生成器以在安全敏感的上下文中使用。 最后,java.util包中的SplittableRandom类针对并行流和 fork/join 式计算进行了优化。

3. 新的 RandomGenerator API

现在,让我们看一下基于RandomGenerator接口的新 API。

3.1. API 设计

新的 API 通过新的接口类型和生成器实现提供了更好的整体设计:

/uploads/java_17_random_number_generators/3.png 在上图中,我们可以看到旧 API 类如何适应新设计。除了这些类型之外,还添加了几个随机数生成器实现类:

  • Xoroshiro集团
    • Xoroshiro128PlusPlus
  • 雄四郎集团
    • Xoshiro256PlusPlus
  • LXM集团
    • L128X1024MixRandom
    • L128X128MixRandom
    • L128X256MixRandom
    • L32X64MixRandom
    • L64X1024MixRandom
    • L64X128MixRandom
    • L64X128星星随机
    • L64X256MixRandom

3.2. 改善领域

API 中缺少接口使得在不同的生成器实现之间切换变得更加困难。因此,第三方很难提供自己的实现。 例如,SplittableRandom与 API的其余部分完全分离,尽管它的某些代码与Random完全相同。 因此,新的RandomGenerator API的主要目标是:

  • 确保不同算法更容易互换
  • 更好地支持基于流的编程
  • 消除现有类中的代码重复
  • 保留旧Random API的现有行为

3.3. 新接口

新的根接口RandomGenerator为所有现有的和新的生成器提供了一个统一的 API。 它定义了返回随机选择的不同类型值的方法,以及随机选择的值流。 新的 API 提供了另外四个新的专用生成器接口:

  • SplitableGenerator允许创建一个新的生成器作为当前生成器的后代
  • JumpableGenerator允许向前跳跃中等数量的平局
  • LeapableGenerator允许向前跳跃大量的平局
  • ArbitrarilyJumpableGenerator将跳跃距离添加到LeapableGenerator

4. RandomGeneratorFactory

新 API 中提供了用于生成特定算法的多个随机数生成器的工厂类。

4.1. 找到所有

RandomGeneratorFactory方法都生成所有可用生成器工厂的非空流。 我们可以使用它来打印所有已注册的生成器工厂并检查其算法的属性

RandomGeneratorFactory.all()
  .sorted(Comparator.comparing(RandomGeneratorFactory::name))
  .forEach(factory -> System.out.println(String.format("%s\t%s\t%s\t%s",
    factory.group(),
    factory.name(),
    factory.isJumpable(),
    factory.isSplittable())));

工厂的可用性是通过通过服务提供者 API 定位RandomGenerator接口的实现来确定的。

4.2. 按属性查找

我们还可以利用all方法通过随机数生成器算法的属性来查询工厂

RandomGeneratorFactory.all()
  .filter(RandomGeneratorFactory::isJumpable)
  .findAny()
  .map(RandomGeneratorFactory::create)
  .orElseThrow(() -> new RuntimeException("Error creating a generator"));

因此,使用 Stream API,我们可以找到满足我们要求的工厂,然后使用它来创建生成器。

5. RandomGenerator选择

除了更新的 API 设计之外,还实现了几种新算法,未来可能还会添加更多算法。

5.1. 选择默认

在大多数情况下,我们没有特定的生成器要求。因此,我们可以直接从RandomGenerator接口获取默认生成器。 这是 Java 17 中推荐的新方法,作为创建Random实例的替代方法:

RandomGenerator generator = RandomGenerator.getDefault();

getDefault方法当前选择L32X64MixRandom生成器。 然而,算法会随着时间而改变。因此,不能保证此方法将在未来版本中继续返回此算法。

5.2. 选择特定

另一方面,当我们确实有特定的生成器要求时,我们可以使用of方法检索特定的生成器

RandomGenerator generator = RandomGenerator.of("L128X256MixRandom");

此方法需要将随机数生成器的名称作为参数传递。 如果未找到命名算法,它将抛出IllegalArgumentException

6. 线程安全

大多数新的生成器实现都不是线程安全的。但是,RandomSecureRandom仍然是。 因此,在多线程环境中,我们可以选择:

  • 共享线程安全生成器的实例
  • 在启动新线程之前从本地源中拆分新实例

我们可以使用SplittableGenerator来实现第二种情况:

List<Integer> numbers = Collections.synchronizedList(new ArrayList<>());
ExecutorService executorService = Executors.newCachedThreadPool();
RandomGenerator.SplittableGenerator sourceGenerator = RandomGeneratorFactory
    .<RandomGenerator.SplittableGenerator>of("L128X256MixRandom")
    .create();
sourceGenerator.splits(20).forEach((splitGenerator) -> {
    executorService.submit(() -> {
        numbers.add(splitGenerator.nextInt(10));
    });
})

这样,我们确保我们的生成器实例以不会产生相同数字流的方式进行初始化。

7. 性能

让我们对 Java 17 中所有可用的生成器实现运行一个简单的性能测试。 我们将使用生成四种不同类型随机数的相同方法测试生成器:

private static void generateRandomNumbers(RandomGenerator generator) {
    generator.nextLong();
    generator.nextInt();
    generator.nextFloat();
    generator.nextDouble();
}

让我们看一下基准测试结果:

算法 模式 分数 错误 单位
L128X1024MixRandom 平均 95,637  ±3,274 ns/op
L128X128MixRandom 平均 57,899  ±2,162 ns/op
L128X256MixRandom 平均 66,095  ±3,260 ns/op
L32X64MixRandom 平均 35,717  ±1,737 ns/op
L64X1024MixRandom 平均 73,690  ±4,967 ns/op
L64X128MixRandom 平均 35,261  ±1,985 ns/op
L64X128星星随机 平均 34,054  ±0,314 ns/op
L64X256MixRandom 平均 36,238  ±0,090 ns/op
随机的 平均 111,369  ±0,329 ns/op
安全随机 平均 9,457,881  ±45,574 ns/op
可拆分随机 平均 27,753  ±0,526 ns/op
Xoroshiro128PlusPlus 平均 31,825  ±1,863 ns/op
Xoshiro256PlusPlus 平均 33,327  ±0,555 ns/op

SecureRandom是最慢的生成器,但这是因为它是唯一的加密强生成器。 由于它们不必是线程安全的,因此与Random相比,新的生成器实现执行得更快