ThreadLocalRandomJDK7J.U.C 包的新成员,它是专门设计用于高并发场景下的随机数生成工具。那么ThreadLocalRandom 为了提高并发能力做了哪些方面的工作呢?

  1. 资源隔离,不采用串行化的方式
  2. 性能提升
    1. 缓存行伪共享
    2. 种子数据粗粒度化

实现思路

资源隔离

之前有比较过 synchronizedThreadLocal 的异同,前者侧重资源访问串行化,后者侧重线程级资源独立化。

  • 我们先看看 Random 处理并发的能力:

    1. private final AtomicLong seed;
    2. protected int next(int bits) {
    3. long oldseed, nextseed;
    4. // 获取当前的种子
    5. AtomicLong seed = this.seed;
    6. do {
    7. // 获取前一个种子
    8. oldseed = seed.get();
    9. // 计算出新的种子
    10. nextseed = (oldseed * multiplier + addend) & mask;
    11. // CAS进行交换
    12. } while (!seed.compareAndSet(oldseed, nextseed));
    13. return (int)(nextseed >>> (48 - bits));
    14. }

    我们可以看到 Random 主要采用 CAS 保证资源同一时间只能被一个线程访问,但若在高并发的场景下,长时间的CAS失败自旋会占用大量的 CPU 资源,这也是为什么出现了 ThreadLocalRandom 的原因。

  • 接下来看看 ThreadLocalRandom 的思路: ```java /**

    • 当前线程初始化 随机数数据 */ static final void localInit() { // 生成probe计数器 int p = probeGenerator.addAndGet(PROBE_INCREMENT); int probe = (p == 0) ? 1 : p; // skip 0 // 生成种子 long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT)); // 获取当前线程 Thread t = Thread.currentThread(); // 将种子和probe数据放入 线程中 UNSAFE.putLong(t, SEED, seed); UNSAFE.putInt(t, PROBE, probe); }

/**

  • 我们通过 current() 方法获取当前线程的ThreadLocalRandom */ public static ThreadLocalRandom current() { // 如果一个线程尚未初始化 if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
    1. // 则执行localInit()初始化当前线程的数据
    2. localInit();
    return instance; } ```

    里面关于 probe、seeder会在后续进行分析

两者在隔离方式上采用了截然不同的方式,那么相应的,应用场景也会有所不同: Random 适用的范围更加广泛,它的粒度更细,可以一个 Random() 全局使用,但是相应的并发能力就会低一些; ThreadLocalRandom 的作用范围通常只在线程内,并发能力高一些。

工作流程

ThreadLocalRandom 结构关系图如下所示:
🎯ThreadLocalRandom - 图1
每个线程都通过 ThreadLocalRandom.current() 初始化当前线程的数据并获得相同的 ThreadLocalRandom

// 这里是 Thread1
public void thread1(){
    ThreadLocalRandom random = ThreadLocalRandom.current();    
}
// 这里是 Thread2
public void thread2(){
    ThreadLocalRandom random = ThreadLocalRandom.current();
}
// 注意,上面获取到的 ThreadLocalRandom 都是相同的对象!

//------------------------------下面是 ThreadLocalRandom 的初始化 ----------------------
static final ThreadLocalRandom instance = new ThreadLocalRandom();
static final void localInit() {
    int p = probeGenerator.addAndGet(PROBE_INCREMENT);
    int probe = (p == 0) ? 1 : p; // skip 0
    long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));
    Thread t = Thread.currentThread();
    UNSAFE.putLong(t, SEED, seed);
    UNSAFE.putInt(t, PROBE, probe);
}

public static ThreadLocalRandom current() {
    if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
        localInit();
    return instance;
}

简单来说, ThreadLocalRandom 就是一个工具对象,全局仅这一个;它将数据绑定在当前线程上,每个线程就通过使用 ThreadLocalRandom 实现各自线程的随机数生成。这里可能会对这种设计方式有疑问,为什么不直接上 ThreadLocal 呢?

  1. ThreadLocal 如果不手动 remove() 仍然有概率发生内存泄漏,作为一个工具,不可能再让用户再调什么类似 destroy() 的方法,麻烦、不友好
  2. 如何设计一个对象无论怎么样调用,一个线程只用同一个对象,不同线程使用不同对象?我想这一定需要 Thread 配合,因为 ThreadLocal 你要么是 static ,全局仅一个;要么就是每次 new 的时候都新建一个 ThreadLocal

我觉得这种设计方式挺独特的,当每个线程只需要一个对象,又想保证线程级隔离,就与 Thread 绑定关系即可。

性能改进

缓存行伪共享消除

前面讲到 ThreadLocalRandom 都是通过获取各自线程的数据从而实现线程间隔离,我们看看官方在 Thread 里面下了什么药:

/** The current seed for a ThreadLocalRandom */
@sun.misc.Contended("tlr")
long threadLocalRandomSeed;
/** Probe hash value; nonzero if threadLocalRandomSeed initialized */
@sun.misc.Contended("tlr")
int threadLocalRandomProbe;
/** Secondary seed isolated from public ThreadLocalRandom sequence */
@sun.misc.Contended("tlr")
int threadLocalRandomSecondarySeed;
  • threadLocalRandomSeedThreadLocalNumber 使用的随机数种子,每个线程持有一份随机数种子从而达到线程间隔离
  • threadLocalRandomProbe ,探针,用来判断当前线程是否已经初始化过随机数
  • threadLocalRandomSecondarySeed ,用来与 threadLocalRandomSeed 隔离,这货主要被 ConcurrentSkipListMapForkJoinPool 等其他并发工具使用;隔离开来的原因是:“避免干扰用户级随机数;其次为了减少线程间的干扰,现在只有用自己线程里的随机数即可”。

    threadLocalRandomSecondarySeed 可以参考 ConcurrentSkipListMap 的注释,搜索 secondary seed 关键字即可找到对应的说明

而这里比较强的点是这个 @sun.misc.Contended("tlr") ,这货可以消除缓存行伪共享。因为 CPU 操作缓存都是针对一个缓存行的,而一个缓存行里可包含多个数据;如果不同的线程的数据都在一个缓存行中,在高并发情况下,就会对缓存行不断进行清理、更新,所以使用该注解可以保证一个缓存行只有一个数据。具体的缓存行伪共享可以参考这篇文章

Unsafe代替反射

因为 ThreadTheadLocalRandom 所在包不一样,而 Thread 里的数据的可见级别又不能是 public ,所以需要一种手段来让 ThreadLocalRandom 获取到 Thread 里面的数据。使用反射?反射性能过于低下,所以打算直接用Unsafe 操作内存来获取数据。

扩展

探针threadLocalRandomProbe的作用

用来特殊标记一个线程,通过 probe 可以获取到当前线程的“身份证”~在 ThreadLocalRandom 中基本就是拿来判断是否初始化过了;其他的并发工具可能会用做他用。

参考资料

  1. 《高并发情况下你还在用Random生成随机数?》
  2. 《Thread Local Randoms in Java》
  3. 《ThreadLocalRandom#getProbe #advanceProbe浅析》