多线程并发编程5-ThreadLocalRandom类源码剖析
今天来说一说ThreadLocalRandom类,ThreadLocalRandom类是juc包下的随机数生成器,它弥补了Random类的不足。下面介绍Random的源码实现、Random存在的问题以及ThreadLocalRandom是如何优化Random存在的缺陷的。
Random
Random类是一个使用广泛的随机生成工具类,下面的代码相信大家一定不陌生,
Random random =new Random();
random.nextInt();
生成一个Random对象实例,通过next系列方法生成对应类型的随机数。next系列方法是如何生成随机数的呢?下面从源码来一探究竟。
nextInt
public int nextInt() {
return next(32);
}
nextInt方法里面调用的是next方法,这就是Random生成随机数的核心。
next
protected int next(int bits) {
long oldseed, nextseed;
AtomicLong seed =this.seed;
do {
oldseed = seed.get(); //(1)
nextseed = (oldseed *multiplier +addend) &mask; //(2)
}while (!seed.compareAndSet(oldseed, nextseed)); //(3)
return (int)(nextseed >>> (48 - bits)); //(4)
}
(1)获得当前种子的值,seed是AtomicLong类型,保证在多线程并发下只会有一个线程设置seed,关于AtomicLong类的原理可以点这里。
(2)根据当前的种子值计算新的种子值。
(3)通过AtomicLong类的CAS算法方法设置seed为新计算的种子值,设置失败则通过自旋的方式知道设置成功为止。
(4)使用固定算法根据新的种子生成随机数。
从上面的代码可以看到在多线性高并发的场景下多个线程同时竞争设置seed,但是设置seed的方法使用的CAS算法,只能有一个线程设置成功,而其他线程都需要通过自旋进行持续尝试直到设置成功。多个线程竞争同一个资源这就是会造成性能下降的原因,这个原因和前面介绍的原子操作类的缺陷是一样的。
ThreadLocalRandom
上面介绍了高并发下Random性能下降的原因是因为多线程对一个资源的竞争,知道问题所在,ThreadLocalRandom解决这个缺陷的方法就是每个线程一个种子,这样避免的多个线程竞争同一个种子带来性能下降的问题。
下面介绍一个ThreadLocalRandom类的使用案例,通过案例调用的方法作为我们源码剖析的切入点。
ThreadLocalRandom random = ThreadLocalRandom.current();
random.nextInt();
current
public static ThreadLocalRandomcurrent() {
if (UNSAFE.getInt(Thread.currentThread(), PROBE) ==0) //(1)
localInit();
return instance;
}
(1)获取当前线程的threadLocalRandomProbe本地变量,如果threadLocalRandomProbe为0,则对当前线程本地变量进行初始化。
localInit
static final void localInit() {
int p =probeGenerator.addAndGet(PROBE_INCREMENT); //(1)
int probe = (p ==0) ?1 : p; // skip 0
long seed =mix64(seeder.getAndAdd(SEEDER_INCREMENT)); //(2)
Thread t = Thread.currentThread();
UNSAFE.putLong(t, SEED, seed); //(3)
UNSAFE.putInt(t, PROBE, probe); //(4)
}
(1)通过CAS算法递增PROBE_INCREMENT变量,并将递增后的值赋值给p,之后利用该值为线程的本地变量threadLocalRandomSeed进行初始化赋值。
(2)通过CAS算法递增SEEDER_INCREMENT变量,并将递增前的值赋给seed,之后利用该值为线程的本地变量threadLocalRandomProbe进行初始化赋值。
(3)(4)为当前线程的threadLocalRandomSeed变量和threadLocalRandomProbe变量进行赋值,这里没有使用锁或是使用CAS进行赋值,是因为当前线程的本地变量不会被其他线程访问,不会造成内存可见性问题,也不会造成竞争问题。
这里多提一点吧,localInit方法源码中可以看见仍然有用CAS+自旋的方法获取变量PROBE_INCREMENT和SEEDER_INCREMENT,但是这只有在获取ThreadLocalRandom实例的时候才会进行这两个变量的操作,所以在实现的代码中使用建议一个线程就获取一次ThreadLocalRandom实例,这样即使在高并发下也不会有什么性能的损失。
nextInt
public int nextInt() {
return mix32(nextSeed()); //(1)
}
(1)获取新的种子并使用固定算法获取随机值。
nextSeed
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); //(1)
return r;
}
(1)获取当前线程的本地变量threadLocalRandomSeed的值,加上常量GAMMA之后,重新设置当前线程的本地变量threadLocalRandomSeed的值。
这里没有使用锁或是使用CAS进行赋值,是因为当前线程的本地变量不会被其他线程访问,不会造成内存可见性问题,也不会造成竞争问题。
今天的分享就到这,有看不明白的地方一定是我写的不够清楚,所有欢迎提任何问题以及改善方法。