ThreadLocalRandom类原理剖析

1.Random类的局限性

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

下面我们先来看一下Random的使用方法。

// 创建一个默认种子的随机数生成器
Random random = new Random();
// 输出10个在0~5(包含0不包含5)之间的随机数
for(int i = 0; i < 10; i++)
	System.out.println(random.nextInt());

首先我们要知道随机数的生成需要一个种子,这个种子其实是一个Long类型的数字。

你可以在使用Random的构造函数的时候指定种子,也可以不指定使用默认的种子。

接下来我们再来看看有了种子以后如何生成随机数。

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

这段代码简而言之就是两个步骤:

  1. 首先根据旧的种子生成新的种子
  2. 根据新的种子生成随机数

如果是在单线程下,这样是没有毛病的。但是如果在多线程的情况下,问题就来了。

多线程情况下,多个线程拿着相同的种子去计算随机数,因为计算随机数的算法是固定的,所以这些拿到相同种子的线程会生成相同的随机数。这不是我们想看到的。所以生成种子的这一步要保证原子性!简单来说就是第一个线程生成新的种子以后,第二个线程就要丢弃旧的种子,根据第一个线程所生成的新种子计算自己的新种子。

所以在上面一段代码中的next()方法中,官方解决办法是,使用原子类AtomicLong中的CAS操作。

下面我们直接看代码

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)); 
	//(9)
	return (int)(nextseed >>> (48 - bits));
}

至此,问题就暴露出来了,更新种子这个过程,在多线程情况下,大量线程自旋重试,会降低并发性能!所以才有了本文的重点ThreadLocalRandom

2.ThreadLocalRandom

看一下如何始使用:

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

3.源码分析

首先看ThreadLocalRandom的类图结构

请添加图片描述

ThreadLocalRandom 中并没有存放具体的种子,具体的种子存放在具体的调用线程的 threadLocalRandomSeed 变量里面。ThreadLocalRandom 类似于 ThreadLocal 类,就是个工具类。当线程调用 ThreadLocalRandomcurrent 方法时,ThreadLocalRandom 负责初始化 调用线程的 threadLocalRandomSeed 变量,也就是初始化种子。

当调用ThreadLocalRandom类中的nextInt方法时,实际上就是获取当前线程中的threadLocalRandomSeed变量来计算新的种子,然后将新的种子更新到当前线程的threadLocalRandomSeed变量中。这里要注意threadLocalRandomSeed是一个普通的Long类型的变量,因为这个变量是线程级别的,所以根本不需要使用原子性变量。

说到这里如果你不理解,可以看看我上一篇博客中关于ThreadLocal的原理。

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实例里面的偏移
量,这个值在后面讲解LongAdder时会用到 
			SECONDARY = UNSAFE.objectFieldOffset
			(tk.getDeclaredField("threadLocalRandomSecondarySeed")); 
		} catch (Exception e) {
			throw new Error(e); 
		}
}

current()方法

该方法用于获取ThreadLocalRandom实例,并初始化调用线程中threadLocalRandomSeedthreadLocalRandomProbe

static final ThreadLocalRandom instance = new ThreadLocalRandom(); 

public static ThreadLocalRandom current() {
    // 该方法首先会判断线程中的threadLocalRandomProbe是否为零(线程中这个值默认为零)
	if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
        // 如果为零,则说明线程是第一次调用该方法,那么就需要初始化当前线程的种子变量
		localInit();
	return instance; 
}

static final void localInit() {
    // 首先使用probeGenerator中的方法计算当前线程中threadLocalRandomProbe的值
	int p = probeGenerator.addAndGet(PROBE_INCREMENT); 
    // skip 0
    int probe = (p == 0) ? 1 : p;
    // 计算当前线程的初始化种子
	long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT)); 
    // 获得当前执行线程
    Thread t = Thread.currentThread();
    // 把上面两个变量设置到当前线程
	UNSAFE.putLong(t, SEED, seed);
	UNSAFE.putInt(t, PROBE, probe);
}

这里为了延迟初始化,在不需要使用随机数功能时就不初始化 Thread 类中的种子变量,这是一种优化。

同时我们需要注意,这是一个静态方法,多个线程返回的是同一个ThreadLocalRandom变量。

int nextInt(int bound)方法

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

少不入川。

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值