是时候放弃使用 ThreadLocalRandom 生成随机数了

大家好,我是大明哥,一个专注「死磕 Java」系列创作的硬核程序员。 本文已收录到我的小站:skjava.com


随机数,这个没有小伙伴没有用过吧,Java 提供了几个用于生成随机数的类,他们使用起来是这么地简单,以至于我们很少去认真的对待随机数的具体结果,就好像它是真的随机一样。

Java 17 之前的伪随机数生成器

在 Java 17 之前,Java 的随机数生成主要依赖于下面两个核心类:

  • java.util.Random
  • java.security.SecureRandom

Random

该类是最最基本的伪随机数生成器,它用于生成一系列不完全是真正随机的数字。Random 提供了多种方法来生成不同类型的随机数,包括整数、长整数、浮点数等。

  • 生成随机整数
ini 复制代码
Random rand = new Random();
int randomInt = rand.nextInt(50); // 生成一个0到49之间的随机整数
  • 生成随机浮点数
ini 复制代码
Random rand = new Random();
double randomDouble = rand.nextDouble(); // 生成一个0.0到1.0之间的随机浮点数

java.util.Random 使用起来很简单,但是它有两个很明显的缺陷。

  1. 具备可观测性

    1. java.util.Random 使用的是一个线性同余生成器(LCG)算法,该算法简单但效率不高。
    2. 可观测这意味着 Random 的输出是完全可预测的,如果我们知道种子和算法,完全可以生成相同的随机序列。这在需要高安全性的随机数生成(如加密)时是不合适的。
  2. 线程不安全 :在多线程环境下,多个线程共享一个 Random 实例可能导致竞争条件和数据不一致。

ThreadLocalRandom

为了解决 Random 的线程不安全问题,Java 推出了 ThreadLocalRandomThreadLocalRandom 是一个适用于多线程环境下生成随机数的生成器,它通过为每个线程提供一个独立的随机数生成器实例来解决线程安全问题。

ThreadLocalRandom 使用线程局部变量(Thread-Local)的概念,每个线程访问 ThreadLocalRandom 时,实际上是访问它自己的一个独立实例,这就着不同线程之间的随机数生成器是完全隔离的,而且他们各自内部的种子是完全隔离的,这就保证了随机数生成的独立性,不会因为其他线程的操作而受到影响。

  • 使用方法
ini 复制代码
ThreadLocalRandom random = ThreadLocalRandom.current();   // 获取 ThreadLocalRandom 实例

int randomInt = random.nextInt(10, 50); // 生成10到49的随机整数
double randomDouble = random.nextDouble(1.0, 5.0); // 生成1.0到5.0的随机浮点数

虽然 ThreadLocalRandom 解决了 Random 多线程的问题,但是它依然是基于线性同余生成器,导致它生成的随机数序仍然是可预测的。

SecureRandom

由于 java.util.Random 生成的随机数具备可预测性,所以它不适用一些安全要求较高的场景。java.security.SecureRandom 是 Java 提供的一个用于生成加密强度随机数的类,它是 java.security 包的一部分,专门为需要高安全性的应用场景设计,比如加密、安全令牌生成、会话密钥以及数字签名应用。

SecureRandom 生成的随机数具有高安全性,这是因为它使用了更加复杂和不可预测的算法,而且这些算法通常都是基于操作系统提供的随机性源,例如 Unix/Linux 系统的 /dev/random/dev/urandom,或 Windows 的 CryptGenRandom API。所以它更加适用于加密和安全相关的领域。

当然,我们也可以手动设置种子,但是一般不推荐这样做,因为会降低随机数的不可预测性。

SecureRandom 也提供了多种方法来生成不同类型的随机数:

ini 复制代码
SecureRandom secureRandom = new SecureRandom();

int randomInt = secureRandom.nextInt();
double randomDouble = secureRandom.nextDouble();

我们还可以指定特定的算法来创建 SecureRandom 实例:

ini 复制代码
SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");

虽然 SecureRandom 生成的随机数具有强大的安全性,但也恰恰如此,导致SecureRandom 在生成随机数时的性能开销比普通的随机数生成器更高。而且在某些情况下,SecureRandom 可能会因为等待足够的熵(随机性)而导致阻塞,尤其是在使用 /dev/random 作为随机性源的系统上。

Java 17 新特性:增强型伪随机数生成器

RandomGenerator 接口

在 Java 17 之前,Java 主要依赖 java.util.Random 和其子类来生成伪随机数。这些生成器在某些应用场景下不够高效或缺乏必要的特性(例如,长期稳定性、非周期性、能够快速跳跃到序列中的任意位置),为了提供多种新的伪随机数生成器,以满足不同的需求,Java 17 引入增强型伪随机数生成器(PRNG)。

Java 17 为随机数提供了一个全新的接口 RandomGenerator,该接口是 Java 生成随机数的顶层接口,用于定义所有伪随机数生成器的标准方法。它是所有新的和旧的随机数的顶层接口,包括 java.util.Random

RandomGenerator 作为顶层接口,为了满足不同的随机数应用需求,它有几个子接口,UML 图例如下:

  • StreamableGenerator:用于那些可以产生随机数流(如整数、长整数、双精度数流)的生成器。实现这个接口的生成器可以创建特定大小或无限大小的随机数流,这对于需要大量随机数的应用(比如模拟或数据分析)特别有用。
  • JumpableGenerator:用于那些可以"跳跃"到其序列中一个远端点的随机数生成器。这意味着生成器可以从当前状态跳跃到一个预测的未来状态,而不必生成所有中间的随机数。这对于并行计算非常有用,因为它允许并行线程生成不重叠的随机数序列。
  • LeapableGenerator:类似于JumpableGenerator,但是提供了更精细控制。实现这个接口的生成器可以"跳跃"到序列中的一个特定点,然后"回跳"到原来的序列。这可以用于更复杂的并行计算场景,其中需要在随机数序列中前进和后退。
  • ArbitrarilyJumpableGenerator:用于那些可以跳到其序列中任意点的生成器。它提供了最灵活的跳跃功能,允许生成器跳到序列中任何位置。这对于需要非常特定随机数序列的应用非常有用。
  • SplittableGenerator:用于可以分裂成两个或多个独立运行的随机数生成器的情况。它允许每个并行组件拥有自己的随机数生成器实例,而这些生成器彼此独立且状态不相关。

RandomGenerator 提供了两类生成随机数的方法:

  1. 生成随机数:例如 nextInt()nextInt(0, 100)nextLong()
  2. 生成随机数流:例如 ints()longs(),这些方法返回的是一个 Stream,对于生成大量随机数比较有用。

Java 提供了多种方式用来创建 RandomGenerator 实例。

  • 使用RandomGenerator.getDefault()

这是获取 RandomGenerator 实例最简单的方式,它返回默认的RandomGenerator实例,适用于大多数用途。

ini 复制代码
RandomGenerator randomGenerator = RandomGenerator.getDefault();
  • 使用RandomGenerator.of(String name)

RandomGenerator 提供了 of() 用于生成特定的随机数生成器,name 为预定义的生成器名称。

ini 复制代码
RandomGenerator randomGenerator = RandomGenerator.of("Xoshiro256PlusPlus");
  • 使用 RandomGeneratorFactory

RandomGeneratorFactory 是生成 RandomGenerator 的工厂类,例如:

ini 复制代码
RandomGeneratorFactory<RandomGenerator> factory = RandomGeneratorFactory.of("Xoshiro256PlusPlus");
RandomGenerator randomGenerator = factory.create();

得到了 RandomGenerator 实例,就可以调用对应的方法获取对应的随机数了。

RandomGeneratorFactory

RandomGeneratorFactory 是创建 RandomGenerator 实例的工具类,可以用它来创建不同类型的 RandomGenerator 实例,包括但不限于传统的线性同余生成器、梅森旋转算法(Mersenne Twister)、Xoroshiro128++ 算法等。同时它还允许用户根据需求定制随机数生成器的行为,比如设置种子或选择特定的算法。

RandomGeneratorFactory 的核心 API 如下:

  1. of(String name):根据指定的名称创建一个 RandomGenerator 实例。
  2. all():返回所有可用的 RandomGeneratorFactory 实例。
  3. getDefault():返回默认的 RandomGeneratorFactory 实例。
  4. create():使用工厂的配置创建一个新的 RandomGenerator 实例。

下面演示如何使用 RandomGeneratorFactory 创建并使用一个随机数生成器:

scss 复制代码
    @Test
    public void randomGeneratorFactoryTest() {
        // 获取一个类型为  Xoroshiro128PlusPlus 的随机数生成器工厂
        RandomGeneratorFactory<RandomGenerator> factory = RandomGeneratorFactory.of("Xoroshiro128PlusPlus");
        // 使用工厂创建一个随机数生成器
        RandomGenerator randomGenerator = factory.create();
        // 生成 10 个随机数
        randomGenerator.ints().limit(10)
                .forEach(System.out::println);
    }

RandomGenerator 类型

Java 17 支持多种 RandomGenerator 类型,我们可以通过 RandomGeneratorFactory.all() 来获取:

csharp 复制代码
    @Test
    public void randomGeneratorFactoryTest() {
        RandomGeneratorFactory.all().forEach(x -> {
            System.out.println(x.name());
        });
    }
 // 结果......
L32X64MixRandom
L128X128MixRandom
L64X128MixRandom
SecureRandom
L128X1024MixRandom
L64X128StarStarRandom
Xoshiro256PlusPlus
L64X256MixRandom
Random
Xoroshiro128PlusPlus
L128X256MixRandom
SplittableRandom
L64X1024MixRandom

其中 SecureRandom 和 Random 是旧的,其余都是 Java 17 提供的,他们位于 jdk.random 模块下:

这些类型都有自己的名字,用注解标注出来,例如:

scala 复制代码
@RandomGeneratorProperties(
        name = "L32X64MixRandom",
        group = "LXM",
        i = 64, j = 1, k = 32,
        equidistribution = 1
)
public final class L32X64MixRandom extends AbstractSplittableWithBrineGenerator {
    //.......
}

我们可以通过 name 属性来获取对应的实例:

csharp 复制代码
    @Test
    public void randomGeneratorTest() {
        RandomGenerator randomGenerator = RandomGenerator.of("L32X64MixRandom");
        randomGenerator.ints().limit(10).forEach(System.out::println);
    }

下面是 Java 17 提供 10 个不同的随机数生成器:

类名 使用的算法 特点
L32X64MixRandom LXM 算法 平衡性能和随机性,适用于多种通用应用
L32X64StarStarRandom StarStar算法 提供较好的随机性和性能,适用于需要较快随机数生成的场景
L64X128MixRandom LXM 算法 提供更长的周期和更高的随机性,适合更复杂的随机数生成需求
L64X128StarStarRandom StarStar算法 结合了较长周期和良好的随机性,适用于复杂应用
L64X256MixRandom LXM 算法 长周期,高随机性,适合于需求严格的随机数生成场景
L64X1024MixRandom LXM 算法 高随机性,适用于特别需要长周期和高随机性的应用
L128X128MixRandom LXM 算法 极长周期,高随机性,适合于高随机质量要求的应用
L128X256MixRandom LXM 算法 提供极长的周期和优秀的随机性,适用于极高随机质量要求的应用
L128X1024MixRandom LXM 算法 极高的随机性和更大的状态空间,适用于对随机数质量有极端要求的场合
Xoshiro256PlusPlus Xoshiro算法 高性能,适用于需要快速、高质量随机数的应用
Xoroshiro128PlusPlus Xoroshiro算法 提供良好的性能和随机性平衡,适用于多种应用场景
相关推荐
m0_571957582 小时前
Java | Leetcode Java题解之第543题二叉树的直径
java·leetcode·题解
魔道不误砍柴功4 小时前
Java 中如何巧妙应用 Function 让方法复用性更强
java·开发语言·python
NiNg_1_2344 小时前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
闲晨4 小时前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
Chrikk5 小时前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*5 小时前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue5 小时前
go语言连续监控事件并回调处理
开发语言·后端·golang
杜杜的man5 小时前
【go从零单排】go语言中的指针
开发语言·后端·golang
测开小菜鸟5 小时前
使用python向钉钉群聊发送消息
java·python·钉钉
P.H. Infinity6 小时前
【RabbitMQ】04-发送者可靠性
java·rabbitmq·java-rabbitmq