1. 概述

生成随机值是软件开发中的常见任务,这也是 Java 提供 java.util.Random 类的原因。

然而,Random 类在多线程环境下的表现并不理想。简而言之,其性能瓶颈主要源于争用(Contention)——当多个线程共享同一个 Random 实例时,会发生资源竞争。

为了解这一限制,Java 在 JDK 7 中引入了 java.util.concurrent.ThreadLocalRandom 类,专为多线程环境下的随机数生成而设计。

本文将探讨 ThreadLocalRandom 的实现原理,以及如何在实际应用程序中正确使用它。

2. ThreadLocalRandom 优于 Random

ThreadLocalRandom 是 ThreadLocal 与 Random 类的结合体,它与当前线程隔离。通过避免对 Random 实例的并发访问,它在多线程环境中能实现更优的性能。

与全局提供随机数的 java.util.Random 不同,ThreadLocalRandom 确保一个线程获得的随机数不受其他线程影响。

此外,与 Random 不同,ThreadLocalRandom 不支持显式设置种子(Seed)。它重写了继承自 Random 的 setSeed(long seed.html) 方法,调用该方法时会始终抛出 UnsupportedOperationException。

2.1 线程争用 (Thread Contention.html)

前文已提到,Random 类在高并发环境中性能较差。为了深入理解这一点,我们可以查看其核心操作 next(int bits.html) 的实现:

private final AtomicLong seed;

protected int next(int bits.html) {
    long oldseed, nextseed;
    AtomicLong seed = this.seed;
    do {
        oldseed = seed.get();
        nextseed = (oldseed * multiplier + addend.html) & mask;
    } while (!seed.compareAndSet(oldseed, nextseed.html));

    return (int.html)(nextseed >>> (48 - bits.html));
}

这是线性同余生成器算法的 Java 实现。显而易见,所有线程都共享相同的 seed 实例变量。

为了生成下一个随机位集,线程首先尝试通过 compareAndSet(简称 CAS)原子性地更新共享的 seed 值。

当多个线程尝试同时使用 CAS 更新 seed 时,只有一个线程会成功,其他线程则会失败。失败的线程将反复重试该过程,直到有机会更新值并最终生成随机数。

虽然该算法是无锁的,且允许不同线程同时进行,但在竞争激烈时,CAS 失败和重试的次数会严重损害整体性能。

另一方面,ThreadLocalRandom 完全消除了这一争用,因为每个线程都拥有自己的 Random 实例及其独立的种子。

接下来,我们将查看生成随机 int、long 和 double 值的一些常用方法。

3. 使用 ThreadLocalRandom 生成随机值

根据 Oracle 文档,我们只需调用 ThreadLocalRandom.current() 方法,即可获取当前线程的 ThreadLocalRandom 实例。随后,通过调用该类的实例方法即可生成随机值。

以下是生成一个无边界随机整数的示例:

int unboundedRandomValue = ThreadLocalRandom.current().nextInt();

接下来,我们看看如何生成有界的随机 int 值,即介于给定下限和上限之间的值。

以下是生成介于 0 到 100 之间的随机 int 值的示例:

int boundedRandomValue = ThreadLocalRandom.current().nextInt(0, 100.html);

请注意,0 是包含下限(inclusive),而 100 为排除上限(exclusive)。

如上例所示,通过调用 nextLong() 和 nextDouble() 方法,我们可以生成 long 和 double 类型的随机值。

Java 8 还添加了 nextGaussian() 方法,用于生成下一个符合正态分布的值,该分布的平均值为 0.0,标准差为 1.0。

与 Random 类一样,我们也可以使用 doubles()、ints() 和 longs() 方法生成随机值流。

4. 使用 JMH 比较 ThreadLocalRandom 与 Random

让我们通过在多线程环境中生成随机值,并使用 JMH(Java Microbenchmark Harness)来比较这两个类的性能。

首先,创建一个所有线程共享一个 Random 实例的示例。我们将生成随机值的任务提交给 ExecutorService:

ExecutorService executor = Executors.newWorkStealingPool();
List<Callable<Integer>> callables = new ArrayList<>();
Random random = new Random();
for (int i = 0; i < 1000; i++.html) {
    callables.add((.html) -> {
         return random.nextInt();
    });
}
executor.invokeAll(callables.html);

使用 JMH 基准测试检查上述代码的性能,结果如下:

# Run complete. Total time: 00:00:36
Benchmark                                            Mode Cnt Score    Error    Units
ThreadLocalRandomBenchMarker.randomValuesUsingRandom avgt 20  771.613 ± 222.220 us/op

同样,我们现在使用 ThreadLocalRandom 代替 Random 实例,它为池中的每个线程使用独立的 ThreadLocalRandom 实例:

ExecutorService executor = Executors.newWorkStealingPool();
List<Callable<Integer>> callables = new ArrayList<>();
for (int i = 0; i < 1000; i++.html) {
    callables.add((.html) -> {
        return ThreadLocalRandom.current().nextInt();
    });
}
executor.invokeAll(callables.html);

使用 ThreadLocalRandom 的测试结果如下:

# Run complete. Total time: 00:00:36
Benchmark                                                       Mode Cnt Score    Error   Units
ThreadLocalRandomBenchMarker.randomValuesUsingThreadLocalRandom avgt 20  624.911 ± 113.268 us/op

通过比较上述针对 Random 和 ThreadLocalRandom 的 JMH 结果,我们可以清楚地看到:使用 Random 生成 1000 个随机值的平均耗时约为 772 微秒,而使用 ThreadLocalRandom 则约为 625 微秒。

因此,我们可以得出结论:在高度并发的环境中,ThreadLocalRandom 效率更高。

如需了解更多关于 JMH 的信息,可参考我们之前的相关文章。

5. 总结

本文说明了 java.util.Random 和 java.util.concurrent.ThreadLocalRandom 之间的区别。

我们展示了 ThreadLocalRandom 在多线程环境中优于 Random 的性能优势,以及如何使用该类生成随机值。

ThreadLocalRandom 虽是对 JDK 的简单补充,但它能在高度并发的应用程序中产生显著的性能提升。

而且,与往常一样,所有这些示例的实现都可以在 github 中找到。

说明:本文内容基于 JDK 7 及以上版本。ThreadLocalRandom 自 JDK 7 引入,部分特性(如流生成方法、nextGaussian)需要 Java 8 或更高版本支持。