ThreadLocalRandom原理剖析
文章目录
- Random 类及其局限性
- ThreadlocalRandom
- 初识ThreadlocalRandom
- 源码分析
- 总结
ThreadLocalRandom 类是 JDK 7 在 JUC 包下新增的随机数生成器,它弥补了 Random类在多线程下的缺陷 。
Random 类及其局限性
在 JDK 7 之前包括现在, java .util.Random 都是使用比较广泛 的随机数生成工具 类,
而且 java.Iang.Math 中的随机数生成也使用的是 java.util.Random 的实例 。下面先看看 java.util.Random 的nextInt()方法。
``
public int nextInt (int bound) {
// (3 )参数检查
if (bound <= 0 )
throw new IllegalArgumentException (BadBound) ;
// (4 )根据老的种子生成新的种子
int r = next (31 );
// ( 5 )根据新的种子计算随机数
…
return r ;
}
``
由此可见,新的随机数的生成需要两个步骤 :
- 首先根据老的种 子生成新的种子 。
- 然后根据新的种子来计算新的随机数 。
其中步骤( 4 ) 我们 可以抽象为 seed=f(seed),其中 f 是 一个固定的函数,比如 seed=f(seed)=a*seed+b :步骤( 5 )也可以抽 象为 g(seed,bound), 其中 g 是 一个 固定的函数,比如 g(seed ,bound)=(int)((bound * (long)seed) >> 31 ) 。 在单线程情况下每次调用 nextlnt 都是根据老的种子计算出新的种子,这是可以保证随机数产生的随机性的 。 但是在多线程下多个线程可能都拿 同 一个老的种子去执行步骤 ( 4 )以计算新的种子,这会导致多个线程产生的新种子是一样的,由于步骤 ( 5 )的算法是固定 的,所 以会导致多个线程产生相同的随机值,这并不是我们想要 的 。 所以步骤( 4 )要保证原子性,也就是说当 多个线程根据同一个老种子计算新种子时, 第一个线程的新种子被计算 出来后,第二个线程要丢弃 自己老的种子,而使用第 一个线程的新种子来计算自己的新种子,依此类推,只有保证了这个,才能保证在多线程下产生的随机数是随机的 。 Random 函数使用 一个原子变量达到了这个效果,在创建 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)
} while (!seed . compareAndSet(oldseed , nextseed) );
// (9) 使用固定算法根据新的种子计算随机数。
return (int) (nextseed >> (48 - bits)) ;
}
``
代码( 8 )使用 CAS 操作,它使用新的种子去更新老的种子 ,在多线程下可能多个线
程都同时执行到了代码( 6 ),那么可能多个线程拿到的当前种子的值是同一个,然后执行步骤( 7 )计算的新种子也都是一样的 ,但是步骤( 8 ) 的 CAS 操作会保证只有一个线程可以更新老的种子为新的 , 失败的线程会通过循环重新获取更新后的种子作为当前种子去计算老的种子,这就解决了上面提到的问题,保证了随机数的随机性 。
总结:每个 Random 实例里面都有一个原子性的种子变量用来记录当前 的 种子值,
当要生成新的随机数时需要根据当前种子计算新的种子并更新回原子变量。在多线程
下使用单个 Random 实例生成随机数时,当 多 个线程 同时计算随机数来计算新的种子
时, 多个线程会竞争 同 一个原子变量的更新操作,由于原子变量 的更新是 CAS 操作,同时只有一个线程会成功,所以会造成大量线程进行自旋重试 , 这会降低并发性能,所以ThreadLocalRandom 应运而生。
ThreadlocalRandom
初识ThreadlocalRandom
从名字上看它会让我们联想到 ThreadLocal : ThreadLocal 通过让每一个线程复制一份变量,使得在每个线程对变量进行操作时实际是操作自己本地内存里面的副本,从而避免 了对共享变量进行同步 。实际上 ThreadLoca!Random 的实现也是这个原理, Random 的缺点是多个线程会使用同 一个原子性种子变量, 从而导致对原子变量更新的竞争。如果每个线程都维护一个种子变量,则每个线程生成随机数时都根据自己老的种子计算新的种子,并使用新种子更新老的种子,再根据新种子计算随机数 , 就不会存在竞争 问题 了,这会大大提高并发性能 。
源码分析
从图中可以 看 出 ThreadLoca!Random 类继承了 Random 类并重写了 nextlnt 方
法 , 在 ThreadLocalRandom 类 中并 没有使 用 继承自 Random 类的原子 性种 子变量 。 在ThreadLocalRandom 中并没有存放具体的种子,具体的种子存放在具体的调用线程的threadLocalRandomSeed 变量 里 面 。 ThreadLocalRandom 类似于 ThreadLocal 类 ,就是个 工具类 。 当线程调用 ThreadLocalRandom 的 current 方法时, ThreadLoca!Random 负责初始化调用线程的 threadLocalRandomSeed 变量 , 也就是初始化种子 。
当 调用 ThreadLocalRandom 的 nextlnt 方法时,实际上是获取 当 前线程的threadLocalRandomSeed 变量作为当前种子来计算新的种子,然后更新新 的种子到当前线程的 threadLocalRandomSeed 变量,而后再根据新种子并使用具体算法计算随机数。这里需要注意的是, threadLocalRandomSeed 变量就是 Thread 类里面的一个普通 long 变量,它并不是原子性变量 。 其实道理很简单,因为这个变量是线程级别的,所以根本不需要使用原子性变量,如果你还是不理解可以思考下 ThreadLocal 的原理 。
其中 seeder 和 probeGenerator 是两个原子性变量,在初始化调用线程的种子和探针变
量时会用 到它们, 每个线程只会使用一次。
另外,变量 instance 是 ThreadLocalRandom 的 一个实例,该变量是 static 的 。当多 线程通过 ThreadLocalRandom 的 current 方法获取 ThreadLocalRandom 的 实例时,其实获取的是同一个实例 。 但是由于具体的种子是存放在线程里面的,所以在 ThreadLocaIRandom的实例里面只包含与线程无关的通用算法, 所以它是线程安全的。
总结
本篇讲了 Random 的实现原理以及 Random 在多线程下需要竞争种子原子变量更新操作的缺点,从而引出 ThreadLocalRandom 类。 ThreadLocalRandom 使用 ThreadLocal
的原理,让每个线程都持有一个本地的种子变量,该种子变量只有在使用随机数时才会被初始化。在多线程下计算新种子时是根据自己线程内维护的种子变量进行更新,从而避免了竞争。