Java并发编程(十六)ThreadLocalRandom

逆流者 2021年02月21日 81次浏览

此文为读书笔记,欢迎评论,谈论问题,共同进步!


ThreadLocalRandom类是 JDK 7在JUC包下新增的随机数生成器,它弥补了Random类在多线程下的缺陷。

Random 类

在JDK 7之前以及现在,java.util.Random 都是使用比较广泛的随机数生成工具类,而且java.lang.Math 中的随机数生成也使用的是java.util.Random的实例。

Random 的多个线程会使用同原子性种子变量, 会导致对原子变量更新竞争问题:
在这里插入图片描述

简单测试下:

public class RandomTest {

    public static void main(String[] args) {
        // 创建一个默认种子的随机数生成器
        Random random = new Random();
        for (int i = 0; i < 10; i++) {
            // random.nextInt(5) 输出一个[0,5)之间的随机数
            System.out.println(random.nextInt(5));
        }
    }
}
4
4
2
2
1
2
1
2
3
0

根据上面的示例我们分析一下源码:
Random.java

private final AtomicLong seed;

public int nextInt(int bound) {
	// 限制上限(不包括)参数检查
    if (bound <= 0)
        throw new IllegalArgumentException(BadBound);
	// 根据老的种子生成新的种子
    int r = next(31);
    // 根据新的种子计算随机数
    int m = bound - 1;
    if ((bound & m) == 0)  // i.e., bound is a power of 2
        r = (int)((bound * (long)r) >> 31);
    else {
        for (int u = r;
             u - (r = u % bound) + m < 0;
             u = next(31))
            ;
    }
    return r;
}

protected int next(int bits) {
    long oldseed, nextseed;
    AtomicLong seed = this.seed;
    do {
    	// 获取当前原子变量种子的值
        oldseed = seed.get();
        // 根据当前种子值计算新的种子
        nextseed = (oldseed * multiplier + addend) & mask;
        // cas操作(原子的), 使用新的种子更新老的种子, 失败的线程会通过循环重新获取更新后的种子作为当前种子去计算老的种子
    } while (!seed.compareAndSet(oldseed, nextseed));
    return (int)(nextseed >>> (48 - bits));
}

搞清楚Random类的原理了吗?

下面我们总结一下:

每个Random实例里面都有一个原子性种子变量用来记录当前种子值,当要生成新的随机数时需要根据当前种子计算新的种子并更新回原子变量。在多线程下使用单个Random实例生成随机数时,当多个线程同时计算随机数来计算新的种子时,多个线程会竞争同一个原子变量的更新操作,由于原子变量的更新是CAS操作,同时只有一个线程会成功,所以会造成大量线程进行自旋重试,这会降低并发性能,所以 ThreadLocalRandom 应运而生。

ThreadLocalRandom

为了弥补多线程高并发情况下 Random 缺陷,JUC 包下新增了 ThreadLocalRandom 类。

每个线程都维护一个种子变量:
在这里插入图片描述

简单使用示例

先来个简单测试:

public class ThreadLocalRandomTest {

    public static void main(String[] args) {
        // 获取一个随机数生成器
        ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current();
        for (int i = 0; i < 10; i++) {
            // threadLocalRandom.nextInt(5) 输出一个[0,5)之间的随机数
            System.out.println(threadLocalRandom.nextInt(5));
        }
    }
}
3
2
0
0
0
4
1
1
4
4

基本分析

根据上面测试代码继续分析。

先看一下ThreadLocalRandom 的类图:

ThreadLocalRandom

是不是觉得ThreadLocalRandom 和 ThreadLocal 很像。

从图中可以看出 ThreadLocalRandom 类继承了Random类并重写了nextInt方法,在 ThreadLocalRandom类中并没有使用继承Random类的原子性种子变量。在ThreadLocalRandom中并没有存放具体的种子,具体的种子存放在具体的调用线程的threadLocalRandomSeed变量里面。ThreadLocalRandom类似于ThreadLocal类,就是个工具类。当线程调用 ThreadLocalRandom 的current方法时,ThreadLocalRandom 负责初始化调用线程的 threadLocalRandomSeed 变量,也就是初始化种子。

当调用 ThreadLocalRandom 的 nextInt方法时,实 际上是获取当前线程的 threadLocalRandomSeed变量作为当前种子来计算新的种子,然后更新新的种子到当前线程的 threadLocalRandomSeed变量,而后再根据新种子并使用具体算法计算随机数。这里需要注意的是,threadLocalRandomSed变量就是Thread 类里面的一个普通long 变量,它并不是原子性变量。其实道理很简单,因为这个变量是线程级别的,所以根本不需要使用原子性变量。

其中 seeder和probeGenerator是两个原子性变量,在初始化调用线程的种子和探针变量时会用到它们,每个线程只会使用一次。

另外,变量 instance是ThreadLocalRandom的一个实例,该变量是 static的。当多线程通过ThreadLocalRandom的current方法获取ThreadLocalRandom的实例时,其实获取的是同一个实例。但是由于具体的种子是存放在线程里面的,所以在ThreadLocalRandom的实例里面只包含与线程无关的通用算法,所以它是线程安全的。

ThreadLocalRandom 的主要实现逻辑

1. Unsafe 机制

// Unsafe mechanics
private static final sun.misc.Unsafe UNSAFE;
private static final long SEED;
private static final long PROBE;
private static final long SECONDARY;
static {
    try {
    	// 获取UNSAFE 实例
        UNSAFE = sun.misc.Unsafe.getUnsafe();
        Class<?> tk = Thread.class;
        // 获取Thread类中threadLocalRandomSeed变量在Thread实例中的偏移量
        SEED = UNSAFE.objectFieldOffset
            (tk.getDeclaredField("threadLocalRandomSeed"));
        // 获取Thread类中threadLocalRandomProbe变量在Thread实例中的偏移量
        PROBE = UNSAFE.objectFieldOffset
            (tk.getDeclaredField("threadLocalRandomProbe"));
        // 获取Thread类中threadLocalRandomSecondarySeed变量在Thread实例中的偏移量
        SECONDARY = UNSAFE.objectFieldOffset
            (tk.getDeclaredField("threadLocalRandomSecondarySeed"));
    } catch (Exception e) {
        throw new Error(e);
    }
}

2. ThreadLocalRandom.current()方法

static final ThreadLocalRandom instance = new ThreadLocalRandom();

public static ThreadLocalRandom current() {
	// 如果当前线程中threadLocalRandomProbe的变量值为0(默认是0),则说明当前线程是第一次调用ThreadLocalRandom.current()方法,那就需要调用 localInit方法计算当前线程的初始化种子变量,这是一种优化(在不需要使用随机数功能时就不初始化Thread类中的种子变量)
    if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
        localInit();
    // instance 是静态,多个线程返回的都是同一个ThreadLocalRandom 实例
    return instance;
}
// 每个线程的初始化探针字段
private static final AtomicInteger probeGenerator = new AtomicInteger();
// 根据 probeGenerator计算当前线程中 threadLocalRandomProbe的初始化值,然后根据 seeder 计算当前线程的初始化种子,而后把这两个变量设置到当前线程。
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);
}

3. int nextInt(int bound)方法

计算当前线程的下一个随机数

public int nextInt(int bound) {
	// // 限制上限(不包括)参数检查
    if (bound <= 0)
        throw new IllegalArgumentException(BadBound);
    // 根据当前线程中的种子计算新种子
    int r = mix32(nextSeed());
    // 根据新种子和bount计算随机数
    int m = bound - 1;
    if ((bound & m) == 0) // power of two
        r &= m;
    else { // reject over-represented candidates
        for (int u = r >>> 1;
             u + m - (r = u % bound) < 0;
             u = mix32(nextSeed()) >>> 1)
            ;
    }
    return r;
}

final long nextSeed() {
    Thread t; long r; // read and update per-thread seed
    // r = UNSAFE.getLong(t, SEED) 这里是获取的当前线程中 threadLocalRandomSeed 变量的值,
    // 然后在种子的基础上累加GAMMA值作为新种子,而后使用UNSAFE的putLong方法把新种子放入当前线程的threadLocalRandomSeed变量中
    UNSAFE.putLong(t = Thread.currentThread(), SEED,
                   r = UNSAFE.getLong(t, SEED) + GAMMA);
    return r;
}

总结

  • Random在多线程下有竞争种子原子变量更新操作的缺点;
  • ThreadLocalRandom使用ThreadLocal的原理,让每个线程都持有一个本地的种子变量,该种子变量只有在使用随机数时才会被初始化。在多线程下计算新种子时是根据自己线程内维护的种子变量进行更新,从而避免了竞争。