1. 概述
生成随机值是编程中的常见需求,Java 提供了 java.util.Random
类来满足这个需求。但在多线程环境下,这个类的表现并不理想。
简单来说,Random
在多线程环境下的性能问题源于线程竞争——多个线程共享同一个 Random
实例。为解决这个限制,Java 在 JDK 7 中引入了 java.util.concurrent.ThreadLocalRandom
类,专门用于多线程环境下的随机数生成。
接下来我们看看 ThreadLocalRandom
的实际表现以及如何在项目中使用它。
2. ThreadLocalRandom 相比 Random 的优势
ThreadLocalRandom
本质上是 ThreadLocal
和 Random
的结合体(后续会详细说明),它被隔离在当前线程中。通过完全避免对 Random
实例的并发访问,它在多线程环境下实现了更好的性能。
每个线程获取的随机数不受其他线程影响,而 java.util.Random
提供的是全局随机数。此外,与 Random
不同,ThreadLocalRandom
不支持显式设置种子。它重写了从 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));
}
这是 线性同余生成器 算法的 Java 实现。显然,所有线程共享同一个 seed
实例变量。
要生成下一组随机比特,它首先尝试通过 CAS(compareAndSet
)原子性地更新共享的 seed
值。当多个线程同时尝试用 CAS 更新 seed
时,只有一个线程会成功更新 seed
,其余线程都会失败。失败的线程会不断重试这个过程,直到有机会更新值并最终生成随机数。
这个算法是无锁的,不同线程可以并发执行。但当竞争激烈时,CAS 失败和重试的次数会严重影响整体性能。
相比之下,ThreadLocalRandom
完全消除了这种竞争,因为每个线程都有自己的 Random
实例和对应的 seed
值。
3. 使用 ThreadLocalRandom 生成随机数
根据 Oracle 文档,只需调用 ThreadLocalRandom.current()
方法,它就会返回当前线程的 ThreadLocalRandom
实例。然后我们可以调用该类的实例方法生成随机值。
生成无界随机 int
值:
int unboundedRandomValue = ThreadLocalRandom.current().nextInt();
生成有界随机 int
值(比如 0 到 100 之间):
int boundedRandomValue = ThreadLocalRandom.current().nextInt(0, 100);
⚠️ 注意:0 是包含的下限,100 是不包含的上限。
类似地,通过调用 nextLong()
和 nextDouble()
方法可以生成 long
和 double
类型的随机值。Java 8 还增加了 nextGaussian()
方法,用于生成均值为 0.0、标准差为 1.0 的正态分布随机值。
与 Random
类一样,我们也可以使用 doubles()
、ints()
和 longs()
方法生成随机值流。
4. 使用 JMH 比较 ThreadLocalRandom 和 Random
下面我们通过 JMH 基准测试,比较在多线程环境下使用这两个类生成随机值的性能差异。
首先是使用共享 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);
使用 ThreadLocalRandom
的结果:
# Run complete. Total time: 00:00:36
Benchmark Mode Cnt Score Error Units
ThreadLocalRandomBenchMarker.randomValuesUsingThreadLocalRandom avgt 20 624.911 ± 113.268 us/op
✅ 对比两组 JMH 结果:使用 Random
生成 1000 个随机值平均耗时 772 微秒,而 ThreadLocalRandom
仅需约 625 微秒。可以明确得出结论:在高并发环境下 ThreadLocalRandom
效率更高。
想深入了解 JMH?可以参考我们之前的这篇文章。
5. 实现细节
将 ThreadLocalRandom
理解为 ThreadLocal
和 Random
的组合是个不错的思维模型。事实上,在 Java 8 之前,这个模型与实际实现是一致的。
但从 Java 8 开始,这种对应关系完全被打破,因为 ThreadLocalRandom
变成了单例。以下是 Java 8+ 中 current()
方法的实现:
static final ThreadLocalRandom instance = new ThreadLocalRandom();
public static ThreadLocalRandom current() {
if (U.getInt(Thread.currentThread(), PROBE) == 0)
localInit();
return instance;
}
共享全局 Random
实例确实会导致高竞争下的性能问题,但为每个线程分配独立实例又显得过度设计。更好的方案是让每个线程只维护自己的 seed
值。从 Java 8 开始,[Thread](https://github.com/openjdk/jdk14u/blob/d48548f5b7713e0d51b107a5e2dfd60383edbd88/src/java.base/share/classes/java/lang/Thread.java#L2059)
类本身就被改造为维护 seed
值:
public class Thread implements Runnable {
// omitted
@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
变量负责维护 ThreadLocalRandom
的当前种子值。此外,辅助种子 threadLocalRandomSecondarySeed
通常被 ForkJoinPool
等内部使用。
这个实现包含了几项优化,进一步提升了 ThreadLocalRandom
的性能:
- 使用
@Contented
注解避免伪共享,通过填充字节将竞争变量隔离到独立缓存行 - 使用
sun.misc.Unsafe
更新这三个变量,而非反射 API - 避免了
ThreadLocal
实现中额外的哈希表查找开销
6. 总结
本文阐述了 java.util.Random
和 java.util.concurrent.ThreadLocalRandom
的核心区别。我们看到了 ThreadLocalRandom
在多线程环境下的优势,包括性能表现和实际使用方法。
ThreadLocalRandom
虽然只是 JDK 的一个小补充,但在高并发应用中能产生显著的性能提升。
所有示例代码的完整实现可以在 GitHub 上找到。