Java ThreadLocalRandom指南
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) 方法,调用该方法时会始终抛出 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 实例及其独立的种子。
接下来,我们将查看生成随机 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);请注意,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++) {
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通过比较上述针对 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 或更高版本支持。
版权声明:本文为原创文章,版权归 戴老师的博客 所有,转载请联系博主获得授权。
本文地址:https://1diff.fun/archives/java-threadlocalrandom-zhi-nan.html
如果对本文有什么问题或疑问都可以在评论区留言,我看到后会尽量解答。