我们首先看Random下的next方法
protected int next(int bits) {
long oldseed, nextseed;
AtomicLong seed = this.seed;
do {
//获取当前原子变量种子的值
//(6)
oldseed = seed.get();
//根据当前种子值计算新的种子
//(7)
nextseed = (oldseed * multiplier + addend) & mask;
//(8)
//使用CAS 操作,它使用新的种子去更新老的种子,在多线程环境下 可能多个线程都同时
} while (!seed.compareAndSet(oldseed, nextseed));
//(9)
return (int)(nextseed >>> (48 - bits));
}
(6) 获取当前原子变量种子的值
(7) 根据当前种子值计算新的种子
(8) 使用CAS操作,它使用新的种子去更新老的种子,在多线程环境下可能多个线程都同时执行到了代码(6),那么可能多个线程拿到的当前种子的值是同一个没然后执行步骤(7)计算的新种子也都是一样的,但是步骤(8)的CAS操作会保证只有一个线程可以更新老的种子为新的,失败的线程会通过循环获取更新后的种子作为当前种子去计算老的种子,这就保证了随机数的随机性。
代码(9)使用固定算法根据新的种子计算随机数。
总结: 在每个Random实例里面都有一个原子性的变量用来记录当前 的种子性,当要生成新的随机数 时需要根据当前种子计算新的种子并更新回原子变量。在多线程下使用单个Random实例生成随机数时,当多个线程同时计算随机数来计算新的种子时,多个线程会竞争同一个原子变量的更新操作,由于原子变量的更新是CAS操作,同时只有一个线程会成功,所以会造成大量线程进行自旋重试,这回降低并发性能,所以ThreadLocalRandom应运而生。
Random的缺点是多个线程会使用同一个原子性种子变量,从而导致原子变量更新的竞争。
那么,如果每个线程都维护一个种子变量,则每个线程生成随机数时都根据自己老的种子计算新的种子,并使用新种子更新老的种子,再根据新种子计算随机数,就不会存在竞争问题了,这会大大提高并发性能,ThreadLocalRandom原理如下图:
我们在看一下ThreadLocalRandom的代码
public int nextInt(int bound) {
if (bound <= 0)
throw new IllegalArgumentException(BadBound);
int r = mix32(nextSeed());
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;
}
我们重点看下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);
return r;
}
如上代码中,首先使用 r = UNSAFE.getLong(t, SEED) + GAMMA
获取当前线程中的threadLocalRandomSeed变量的值,然后在种子的基础上累加GAMMA值作为新种子,而后使用UNSAFE的putLong方法把新种子放入当前线程的threadLocalRandomSeed变量中。
总结
ThreadLocalRandom
使用ThreadLocal的原理,让每个线程都持有一个本地的种子变量,该种子变量只有在使用随机数时才会被初始化。在多线程计算新种子时是根据自己线程内维护的种子变量进行更新,从而避免了竞争。
史上最全的并发编程脑图:https://www.processon.com/view/5d43e6cee4b0e47199351b7f