1. 概述

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

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

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

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

2. ThreadLocalRandom 优于 Random

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

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

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

2.1 线程争用 (Thread Contention)

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

private final AtomicLong seed;

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

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

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

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

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

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

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

接下来,我们将查看生成随机 intlongdouble 值的一些常用方法。

3. 使用 ThreadLocalRandom 生成随机值

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

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

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

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

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

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

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

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

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++) {
    callables.add(() -> {
         return random.nextInt();
    });
}
executor.invokeAll(callables);

使用 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++) {
    callables.add(() -> {
        return ThreadLocalRandom.current().nextInt();
    });
}
executor.invokeAll(callables);

使用 ThreadLocalRandom 的测试结果如下:

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

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

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

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

5. 总结

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

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

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

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

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