ThreadLocalRandom

译文,跳转原文 (opens new window)

# 概述

生成随机数是一种非常常见的任务。这也是Java提供Random类的原因。

然而Random在多线程环境中性能不高。

简单来说,性能不高的原因是,多个线程共享了一个Random实例。

为了解决这个问题,JDK7中引入了ThreadLocalRandom,用于在多线程环境生成随机数。

# ThreadLocalRandom优于Random

ThreadLocalRandomThreadLocalRandom组成,并隔离在线程中。因此,它通过避免对Random实例的任何并发访问来在并发环境中提供更好性能。

一个线程获得的随机数不受另一个线程影响。然而,Random提供的是全局随机数。

不同于RandomThreadLocalRandom不支持直接设置随机数种子。它重写了RandomsetSeed方法,调用该方法会抛出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));
}
1
2
3
4
5
6
7
8
9
10
11
12

该方法使用了线性同余算法,可以明显看到,所有线程共享种子变量。

为了生生下一组随机数,它首先尝试通过compareAndSet 或简称CAS以原子方式更改共享种子值。

当多线程使用CAS更新种子值,一个线程成功更新,其他线程失败。其他线程不断重试直到更新种子并生成随机数。

算法是无锁的,不同线程可以同时执行。但是,当并发度较高时,CAS失败和重试次数会显著降低整体性能。

另一方面,ThreadLocalRandom则完全避免了竞争,因为每一个线程有单独的随机数发生器和种子。

下面让我们看看一些生成随机数intdoublelong值的方法。

# 使用ThreadLocalRandom生成随机数

根据Oracle文档描述,我们只需要调用ThreadLocalRandom.current()方法,即可获得当前线程的ThreadLocalRandom实例,然后调用类实例方法生成随机数。

生成无界int随机数:

int unboundedRandomValue = ThreadLocalRandom.current().nextInt());
1

生成有界int随机数:

int boundedRandomValue = ThreadLocalRandom.current().nextInt(0, 100);
1

需要注意,这个方法生成的随机数范围,包含左边界,不包含右边界。

同样地,与上面的例子一样,我们可以使用nextLong()nextDouble()方法生成longdouble类型随机数。

Java 8还添加了nextGaussian()方法来生成正态分布值,该值与生成器序列的平均值为 0.0,标准差为 1.0

Random类一样,我们也可以使用ints()doubles()longs()生成随机数流。

# 使用JMH比较ThreadLocalRandomRandom

让我们看看如何在多线程环境,使用这两个类生成随机数,并使用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);
1
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
1
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);
1
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
1
2
3

最后,通过JMH基准测试结果,可以明显看到生成一千个随机数,Random平均用时772毫秒,ThreadLocalRandom平均用时625毫秒。

所以我们可以得出结论,并发环境中,ThreadLocalRandomRandom更高效。

# 实现细节

ThreadLocalRandom视为ThreadLocalRandom的组合是一个很好的思维模型。事实上,在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;
}
1
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;
}
1
2
3
4
5
6
7
8
9
10
11
12

threadLocalRandomSeed变量负责维护 ThreadLocalRandom的当前种子值。此外,辅助种子 threadLocalRandomSecondarySeed通常由 ForkJoinPool等内部使用。

此实现包含一些优化以使 ThreadLocalRandom性能更高:

  1. 使用Contented注解,避免错误共享 (opens new window):增加足够的填充变量,将并发度高的变量隔离到单独的CPU缓存行

  2. 使用 sun.misc.Unsafe 更新这三个变量,而不是使用反射 API

  3. 避免与 ThreadLocal 实现相关的额外哈希表查找

# 总结

本文展示了RandomThreadLocalRandom之间的区别。

我们还看到了ThreadLocalRandom在并发环境中优于Random的优势、性能以及如何使用它生成随机数。

ThreadLocalRandom是 JDK 的一个简单补充,但它可以在高并发应用程序中产生显着影响。

最后,与往常一样,所有这些示例的实现都可以在 GitHub (opens new window)上找到。

上次更新: 2023/04/09, 16:34:32
最近更新
01
go-admin-ui项目仿写练手1-登录页
06-29
02
maven依赖问题
06-17
03
JVM相关命令
02-21
更多文章>