Java中的ThreadLocalRandom指南

2023/06/07

1. 概述

生成随机值是一项非常常见的任务。这就是Java提供java.util.Random类的原因。

但是,这个类在多线程环境中表现不佳

简而言之,Random在多线程环境中性能不佳的原因是由于争用-假设多个线程共享同一个Random实例。

为了解决这个限制,Java在JDK 7中引入了java.util.concurrent.ThreadLocalRandom类-用于在多线程环境中生成随机数

让我们看看ThreadLocalRandom是如何执行的,以及如何在实际应用程序中使用它。

2. ThreadLocalRandom优于Random

ThreadLocalRandom是ThreadLocal和Random类的组合(稍后将详细介绍),并且与当前线程隔离。因此,它通过简单地避免对Random实例的任何并发访问,在多线程环境中实现了更好的性能。

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

此外,与Random不同,ThreadLocalRandom不支持显式设置种子。相反,它重写了从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));
}

这是线性同余生成器算法的Java实现。很明显,所有线程都共享同一个seed实例变量。

为了生成下一个随机比特,它首先尝试通过compareAndSet或简称CAS以原子方式更改共享种子(seed)值。

当多个线程尝试使用CAS并发更新种子时,一个线程获胜并更新种子,其余线程则失败。丢失的线程将一遍又一遍地尝试相同的过程,直到它们有机会更新值并最终生成随机数

该算法是无锁的,不同线程可以并发进行。然而,当争用较高时,CAS失败和重试的次数将显著影响总体性能

另一方面,ThreadLocalRandom完全消除了这种争用,因为每个线程都有自己的Random实例,因此也有自己的受限种子。

现在让我们看一下生成随机int、long和double值的一些方法。

3. 使用ThreadLocalRandom生成随机值

根据Oracle文档,我们只需要调用ThreadLocalRandom.current()方法,它将返回当前线程的ThreadLocalRandom实例。然后,我们可以通过调用类的可用实例方法来生成随机值。

让我们生成一个无边界的随机int值:

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

接下来,让我们看看如何生成一个随机有界int值,这意味着一个介于给定下限和上限之间的值。

下面是生成0到100之间的随机int值的示例:

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

请注意,0是包含的下限,100是排除的上限。

我们可以通过调用nextLong()和nextDouble()方法来为long和double生成随机值,方法与上面的示例类似。

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

与Random类一样,我们也可以使用doubles()、ints()和longs()方法来生成随机值流。

4. 使用JMH比较ThreadLocalRandom和Random

让我们看看如何使用这两个类在多线程环境中生成随机值,然后使用JMH比较它们的性能。

首先,让我们创建一个示例,其中所有线程共享一个Random实例。在这里,我们将使用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. 实现细节

将ThreadLocalRandom视为ThreadLocal和Random类的组合是一个很好的理解模型。事实上,这种模型与Java 8之前的实际实现是一致的。

然而,从Java 8开始,随着ThreadLocalRandom成为单例,这就变得基本不一样了。下面是current()方法在Java 8+中的形式:

static final ThreadLocalRandom instance = new ThreadLocalRandom();

public static ThreadLocalRandom current() {
    if (U.getInt(Thread.currentThread(), PROBE) == 0)
        localInit();

    return instance;
}

的确,共享一个全局Random实例会在高争用情况下导致次优性能。但是,为每个线程使用一个专用实例也是一种矫枉过正的做法。

每个线程只需要维护自己的种子值,而不是每个线程的专用Random实例。从Java 8开始,Thread类本身已经过改造以维护种子值:

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;
}

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

此实现包含了一些优化,以使ThreadLocalRandom具有更好的性能:

  • 通过使用@Contented注解避免虚假共享,该注解基本上添加了足够的填充(padding)以将争用变量隔离在它们自己的缓存行中
  • 使用sun.misc.Unsafe来更新这三个变量,而不是使用反射API
  • 避免与ThreadLocal实现相关联的额外哈希表查找

6. 总结

本文说明了java.util.Random和java.util.concurrent.ThreadLocalRandom之间的区别。

我们还看到了ThreadLocalRandom在多线程环境中相对于Random的优势,以及性能和我们如何使用该类生成随机值。

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

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章