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 中一个简单的新增类,但在高并发应用中,它能带来显著的性能提升。