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)上找到。