Java并发编程——ThreadLocalRandom原理解析

PunkLu 2020年01月12日 123次浏览
ThreadLocalRandom原理解析

ThreadLocalRandom类原理剖析

Random类及其局限性

在JDK 7之前包括现在,java.util.Random都是使用比较广泛的随机数生成工具类。

Random类使用方法:

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

代码1创建一个默认随机数生成器,并使用默认的种子。

代码2输出10个在0-5(包含0,不包含5)之间的随机数。

随机数的生成需要一个默认的种子,这个种子是一个long型的数字,可以在创建Random对象时通过构造函数指定,如果不指定则在默认构造函数内部生成一个默认的值。

生成随机数的代码:

public int nextInt(int bound){
	// 3、参数检查
	if(bound <= 0){
		throw new IllegalArgumentException(BadBound);
	}
	// 4、根据老的种子生成新的种子
	int r = next(31);
	// 5、根据新的种子计算随机数
	...
	return r;
}

由此可见,新的随机数的生成需要两个步骤:

  1. 首先根据老的种子生成新的种子
  2. 然后根据新的种子来计算新的随机数

在单线程情况下每次调用nextInt都是根据老的种子计算出新的种子,这是可以保证随机数产生的随机性的。但是在多线程下多个线程可能都拿同一个老的种子去执行步骤4以计算新的种子,这会导致多个线程产生的新种子是一样的,由于步骤5的算法是固定的,所以会导致多个线程产生相同的随机数,所以步骤4需要保证原子性才能保证多线程下产生的随机数是随机的。Random使用了一个原子变量达到了这个效果,并在计算种子的next(int bits)方法里使用了CAS算法进行保证:

next(int bits)方法源码:

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

但是由于原子变量的更新是CAS操作,同时只有一个线程会成功,所以会造成大量线程进行自旋重试,这会降低并发性能,所以ThreadLocalRandom应运而生。

ThreadLocalRandom

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

ThreadLocalRandom类使用方法:

public class RandomTest{

	public static void main(String[] args){
		// 10、获取一个随机数生成器
		ThreadLocalRandom random = ThreadLocalRandom.current();
		
		// 11、输出10个在0-5(包含0,不包含5)之间的随机数
		for(int i = 0;i < 10;++i){
			System.out.println(random.nextInt(5));
		}
	}
}

ThreadLocalRandom和跟其名字相似的ThreadLocal类的原理是一样的:让每个线程复制一份变量,使得在每个线程对变量进行操作时实际是操作自己本地内存里面的副本,从而避免了对共享变量进行同步。Random的缺点是多个线程会使用同一个原子性种子变量,从而导致对原子变量更新的竞争。那么如果每个线程都维护一个中种子变量,则每个线程生成随机数时都根据自己老的种子计算新的种子,并使用新种子更新老的种子,再根据新种子计算随机数,就不会存在竞争问题了,这会大大提高并发性能。

源码分析

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

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

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

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

ThreadLocalRandom的主要代码的实现逻辑:

1、Unsafe机制

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()方法

该方法获取ThreadLocalRandom实例,并初始化调用线程中的threadLocalRandomSeed和threadLocalRandomProbe变量。

static final ThreadLocalRandom instance = new ThreadLocalRandom();

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

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

在代码12中,如果当前线程中threadLocalRandomProbe的变量值为0(默认情况下线程的这个变量值为0),则说明当前线程是第一次调用ThreadLocalRandom的current方法,那么就需要调用localInit方法计算当前线程的初始化种子变量。这里使用的延迟初始化,不使用随机数功能时就不初始化Thread类中的种子变量。

代码13首先根据probeGeneraor计算当前线程中threadLocalRandomProbe的初始化值,然后根据seeder计算当前线程的初始化种子,而后把这两个变量设置到当前线程。代码14返回ThreadLocalRandom的实例。需要注意的是,这个方法是静态方法,多个线程返回的是同一个ThreadLocalRandom实例。

3、int nextInt(int bound)方法

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

public int nextInt(int bound) {
		// 15 参数校验
        if (bound <= 0)
            throw new IllegalArgumentException(BadBound);
        // 16 根据当前线程中的种子计算新种子
        int r = mix32(nextSeed());
        // 17 根据新种子和bound计算随机数
        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
        UNSAFE.putLong(t = Thread.currentThread(), SEED,
                       r = UNSAFE.getLong(t, SEED) + GAMMA);
        return r;
}

可以看到nextSeed()方法中,首先使用r = UNSAFE.getLong(t,SEED)获取当前线程中threadLocalRandomSeed变量的值,然后在种子的基础上累加GAMMA值作为新种子,而后使用UNSAFE的putLong方法把新种子放入当前线程的threadLocalRandomSeed变量中。