Java 中 ThreadLocalRandom 使用指南

更新于 2025-12-29

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 提供的是全局共享的随机数序列。

此外,与 Random 不同,ThreadLocalRandom 不支持显式设置种子(seed)。它重写了从 Random 继承的 setSeed(long seed) 方法,一旦调用就会抛出 UnsupportedOperationException。

2.1 线程竞争问题

我们已经知道 Random 类在高并发环境下表现不佳。为了更好地理解这一点,让我们看看其核心方法之一 next(int) 的实现:

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));
}

这是线性同余生成器(Linear Congruential Generator, LCG)算法的 Java 实现。很明显,所有线程都共享同一个 seed 实例变量。

为了生成下一组随机比特位,它首先尝试通过 compareAndSet(简称 CAS)原子地修改共享的 seed 值。

当多个线程同时使用 CAS 更新 seed 时,只有一个线程能成功更新,其余线程失败并不断重试,直到成功更新并生成随机数。

该算法是无锁的(lock-free),允许多个线程并发执行。但在高竞争场景下,频繁的 CAS 失败和重试会显著降低整体性能。

相比之下,ThreadLocalRandom 完全消除了这种竞争,因为每个线程都有自己的 Random 实例(更准确地说,是自己的种子),彼此互不干扰。

接下来,我们看看如何使用 ThreadLocalRandom 生成 int、long 和 double 类型的随机值。


3. 使用 ThreadLocalRandom 生成随机值

根据 Oracle 官方文档,只需调用 ThreadLocalRandom.current() 方法,即可获得当前线程的 ThreadLocalRandom 实例。然后就可以调用该实例的方法来生成随机值。

无界随机整数

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

有界随机整数(指定范围)

例如,生成一个介于 0(含)到 100(不含)之间的随机整数:

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

注意:下界是包含的,上界是不包含的。

类似地,也可以通过 nextLong() 和 nextDouble() 方法生成 long 和 double 类型的随机值。

Java 8 还新增了 nextGaussian() 方法,用于生成均值为 0.0、标准差为 1.0 的高斯(正态)分布随机数。

与 Random 类似,ThreadLocalRandom 也支持 doubles()、ints() 和 longs() 方法,用于生成随机数流(Stream)。


4. 使用 JMH 对比 ThreadLocalRandom 与 Random 的性能

下面我们通过 JMH(Java Microbenchmark Harness)对比在多线程环境下两种方式的性能。

使用共享的 Random 实例

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

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);

JMH 测试结果:

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

性能对比结论

  • 使用 Random 生成 1000 个随机数平均耗时约 772 微秒
  • 使用 ThreadLocalRandom 平均耗时约 625 微秒

由此可见,在高并发场景下,ThreadLocalRandom 明显更高效。

想深入了解 JMH?请参阅我们之前的文章。


5. 实现细节

将 ThreadLocalRandom 理解为 ThreadLocal 与 Random 的组合是一种不错的思维模型。事实上,在 Java 8 之前,这种理解与其实现基本一致。

但从 Java 8 开始,这种对应关系被彻底打破:ThreadLocalRandom 变成了一个单例(singleton)。

以下是 Java 8+ 中 current() 方法的实现:

static final ThreadLocalRandom instance = new ThreadLocalRandom();

public static ThreadLocalRandom current() {
    if (U.getInt(Thread.currentThread(), PROBE) == 0)
        localInit();

    return instance;
}

虽然共享一个全局 Random 实例在高竞争下性能不佳,但为每个线程分配一个完整的 Random 实例也是一种资源浪费。

实际上,每个线程只需要维护自己的**种子值(seed)**即可。

因此,从 Java 8 起,Thread 类本身被改造,直接内嵌了种子字段:

public class Thread implements Runnable {
    // 省略其他代码

    @jdk.internal.vm.annotation.Contended("tlr")
    long threadLocalRandomSeed;

    @jdk.internal.vm.annotation.Contended("tlr")
    int threadLocalRandomProbe;

    @jdk.internal.vm.annotation.Contended("tlr")
    int threadLocalRandomSecondarySeed;
}
  • threadLocalRandomSeed:用于保存当前线程的主种子。
  • threadLocalRandomSecondarySeed:通常由 ForkJoinPool 等内部使用。

这种实现带来了多项优化:

  • 使用 @Contended 注解避免伪共享(false sharing),通过填充(padding)确保这些变量各自独占缓存行。
  • 使用 sun.misc.Unsafe 直接操作这些字段,避免反射开销。
  • 避免了传统 ThreadLocal 实现中的哈希表查找开销。

6. 结论

本文详细对比了 java.util.Random 与 java.util.concurrent.ThreadLocalRandom 的区别。

我们展示了在多线程环境中,ThreadLocalRandom 相较于 Random 的显著性能优势,并演示了如何使用它生成各种类型的随机值。

尽管 ThreadLocalRandom 只是 JDK 中一个简单的新增类,但在高并发应用中,它能带来显著的性能提升。