ThreadLocalRandom
# 概述
生成随机数是一种非常常见的任务。这也是Java
提供Random
类的原因。
然而Random
在多线程环境中性能不高。
简单来说,性能不高的原因是,多个线程共享了一个Random
实例。
为了解决这个问题,JDK7
中引入了ThreadLocalRandom
,用于在多线程环境生成随机数。
# ThreadLocalRandom
优于Random
ThreadLocalRandom
由ThreadLocal
和Random
组成,并隔离在线程中。因此,它通过避免对Random
实例的任何并发访问来在并发环境中提供更好性能。
一个线程获得的随机数不受另一个线程影响。然而,Random
提供的是全局随机数。
不同于Random
,ThreadLocalRandom
不支持直接设置随机数种子。它重写了Random
的setSeed
方法,调用该方法会抛出UnsupportedOperationException
异常。
# 线程争用
现在,我们已经知道了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));
}
2
3
4
5
6
7
8
9
10
11
12
该方法使用了线性同余算法,可以明显看到,所有线程共享种子变量。
为了生生下一组随机数,它首先尝试通过compareAndSet
或简称CAS
以原子方式更改共享种子值。
当多线程使用CAS
更新种子值,一个线程成功更新,其他线程失败。其他线程不断重试直到更新种子并生成随机数。
算法是无锁的,不同线程可以同时执行。但是,当并发度较高时,CAS
失败和重试次数会显著降低整体性能。
另一方面,ThreadLocalRandom
则完全避免了竞争,因为每一个线程有单独的随机数发生器和种子。
下面让我们看看一些生成随机数int
、double
、long
值的方法。
# 使用ThreadLocalRandom
生成随机数
根据Oracle
文档描述,我们只需要调用ThreadLocalRandom.current()
方法,即可获得当前线程的ThreadLocalRandom
实例,然后调用类实例方法生成随机数。
生成无界int
随机数:
int unboundedRandomValue = ThreadLocalRandom.current().nextInt());
生成有界int
随机数:
int boundedRandomValue = ThreadLocalRandom.current().nextInt(0, 100);
需要注意,这个方法生成的随机数范围,包含左边界,不包含右边界。
同样地,与上面的例子一样,我们可以使用nextLong()
和nextDouble()
方法生成long
和double
类型随机数。
Java 8
还添加了nextGaussian()
方法来生成正态分布值,该值与生成器序列的平均值为 0.0
,标准差为 1.0
。
与Random
类一样,我们也可以使用ints()
、doubles()
、longs()
生成随机数流。
# 使用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);
2
3
4
5
6
7
8
9
让我们使用 JMH 基准测试检查上面代码的性能:
# Run complete. Total time: 00:00:36
Benchmark Mode Cnt Score Error Units
ThreadLocalRandomBenchMarker.randomValuesUsingRandom avgt 20 771.613 ± 222.220 us/op
2
3
同样地,使用ThreadLocalRandom
代理Random
实例:
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);
2
3
4
5
6
7
8
ThreadLocalRandom
的性能表现:
# Run complete. Total time: 00:00:36
Benchmark Mode Cnt Score Error Units
ThreadLocalRandomBenchMarker.randomValuesUsingThreadLocalRandom avgt 20 624.911 ± 113.268 us/op
2
3
最后,通过JMH
基准测试结果,可以明显看到生成一千个随机数,Random
平均用时772
毫秒,ThreadLocalRandom
平均用时625
毫秒。
所以我们可以得出结论,并发环境中,ThreadLocalRandom
比Random
更高效。
# 实现细节
将ThreadLocalRandom
视为ThreadLocal
和Random
的组合是一个很好的思维模型。事实上,在JDK8
之前也确实是这样实现的。
到了JDK8
,这个类被重构了,ThreadLocalRandom
成为单例对象。
下面是JDK8
中的corrent()
方法:
static final ThreadLocalRandom instance = new ThreadLocalRandom();
public static ThreadLocalRandom current() {
if (U.getInt(Thread.currentThread(), PROBE) == 0)
localInit();
return instance;
}
2
3
4
5
6
7
8
确实,在高并发环境中共享一个Random
实例性能欠佳,但是,每个线程使用一个单独的实例也不是好的选择。
取而代之的是每个线程维护一个随机数种子。在JDK8
中,线程本身被改造成维护随机数种子。
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;
}
2
3
4
5
6
7
8
9
10
11
12
threadLocalRandomSeed
变量负责维护 ThreadLocalRandom
的当前种子值。此外,辅助种子 threadLocalRandomSecondarySeed
通常由 ForkJoinPool
等内部使用。
此实现包含一些优化以使 ThreadLocalRandom
性能更高:
使用
Contented
注解,避免错误共享 (opens new window):增加足够的填充变量,将并发度高的变量隔离到单独的CPU
缓存行使用
sun.misc.Unsafe
更新这三个变量,而不是使用反射 API避免与
ThreadLocal
实现相关的额外哈希表查找
# 总结
本文展示了Random
和ThreadLocalRandom
之间的区别。
我们还看到了ThreadLocalRandom
在并发环境中优于Random
的优势、性能以及如何使用它生成随机数。
ThreadLocalRandom
是 JDK 的一个简单补充,但它可以在高并发应用程序中产生显着影响。
最后,与往常一样,所有这些示例的实现都可以在 GitHub (opens new window)上找到。