Java JUC ThreadLocalRandom类解析

ThreadLocalRandom 类解析 前言 ThreadLocalRandom 类是 JDK7 在JUC包下新增的随机数生成器,它主要解决了 Random 类在多线程下的不足。
本文主要讲解为什么需要 ThreadLocalRandom 类,以及该类的实现原理。
Random 类及其局限性 首先我们先了解一下 Random 类。在 JDK7 以前到现在,java.util.Random 类都是使用较为广泛的随机数生成工具类,而且 java.lang.Math 的随机数生成也是使用的 java.util.Random 类的实例,下面先看看如何使用 Random 类。

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.print(random.nextInt(5)); //3421123432 } }

随机数的生成需要一个默认种子,这个种子其实是一个long类型的数字,可以通过在创建 Random 类对象时通过构造函数指定,如果不指定则在默认构造函数内部生成一个默认的值。
种子数只是随机算法的起始数字,和生成的随机数字的区间无关。
public Random() { this(seedUniquifier() ^ System.nanoTime()); } public Random(long seed) { if (getClass() == Random.class) this.seed = new AtomicLong(initialScramble(seed)); else { // subclass might have overriden setSeed this.seed = new AtomicLong(); setSeed(seed); } }

在有了默认种子之后,Random 是如何生成随机数的呢?我们看一下 nextInt()方法。
public int nextInt(int bound) { //3. 参数检查 if (bound <= 0) throw new IllegalArgumentException(BadBound); //4. 根据老的种子生成新的种子 int r = next(31); //5. 根据新的种子计算随机数 int m = bound - 1; if ((bound & m) == 0)// i.e., bound is a power of 2 r = (int)((bound * (long)r) >> 31); else { for (int u = r; u - (r = u % bound) + m < 0; u = next(31)) ; } return r; }

由此可见,新的随机数的生成需要两个步骤:
  • 根据老的种子生成新的种子
  • 然后根据新的种子来计算新的随机数
其中步骤4可以抽象为seed = f(seed),比如seed = f(seed) = a*seed+b
步骤5可以抽象为g(seed,bound),比如g(seed,bound) = (int) ((bound * (long) seed) >> 31)
在单线程情况下每次调用nextInt()都是根据老的种子计算出新的种子,这是可以保证随机数产生的随机性。但在多线程下多个线程可能拿同一个老的种子去执行步骤4以计算新的种子,这会导致多个线程的新种子是一样的,并且由于步骤5的算法是固定的,所以会导致多个线程产生相同的随机值。
所以步骤4要保证原子性,也就是说当多个线程根据同一个老种子计算新种子时,第一个线程的新种子被计算出来后,第二个线程要丢弃自己老的种子,而使用第一个线程的新种子来计算自己的新种子,依此类推。
在 Random 中使用了 原子变量 AtomicLong来达到这个效果, 在创建 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)); return (int)(nextseed >>> (48 - bits)); }

总结:在多线程下虽然同一时刻只有一个线程会成功,但是会造成大量线程进行自旋操作,这样会降低并发性能,所以 ThreadLocalRandom 运用而生。
ThreadLocalRandom 为了弥补 Random 在多线程下的效率问题,在 JUC 包中增加了 ThreadLocalRandom 类,下面先演示如何使用 ThreadLocalRandom 类:
public static void main(String[] args) { //1. 获取一个随机数生成器 ThreadLocalRandom random = ThreadLocalRandom.current(); //2. 输出10个在0-5(包含0,不包含5)之间的随机数 for (int i = 0; i < 10; ++i) { System.out.print(random.nextInt(5)); }}

其中第一步调用ThreadLocalRandom.current()来获取当前线程的随机数。
实际上 ThreadLocalRandom 的实现和 ThreadLocal 差不多,都是通过让每一个线程复制一份变量,从而让每个线程实际操作的都是本地内存中的副本,避免了对共享变量的同步。
Random 的缺点是多个线程会使用同一个原子性种子变量,从而导致对原子变量更新的竞争。
而在 ThreadLocalRandom 中,每个线程都维护一个种子变量,在每个线程生成随机数的时候都根据当前线程中旧的种子去计算新的种子,并使用新的种子更新老的种子,再根据新的种子去计算随机数,这样就不会存在竞争问题了。
Java JUC ThreadLocalRandom类解析
文章图片

源码解析 首先我们先看一下 ThreadLocalRandom 的类图结构。
Java JUC ThreadLocalRandom类解析
文章图片

从图中可以看到 ThreadLocalRandom 继承了 Random 类并重写了nextInt()方法,在 ThreadLocalRandom 类中并没有使用 Random 的原子性种子变量。
在 ThreadLocalRandom 中并没有存放具体的种子,具体的种子都放在具体的调用线程的 ThreadLocalRandomSeed 中变量中。
ThreadLocalRandom 类似于 ThreadLocal 类,是个工具类。当线程调用 ThreadLocalRandom 的 current()方法时,ThreadLocalRandom 负责初始化调用线程的 threadLocalRandomSeed 变量,进行初始化种子。
【Java JUC ThreadLocalRandom类解析】在调用 ThreadLocalRandom 的nextInt()方法时,实际就是获取当前线程的 ThreadLocalRandomSeed 变量作为当前种子去计算新种子,然后更新新的种子到 ThreadLocalRandomSeed 中,随后再根据新的种子去计算随机数。
需要注意的是:threadLocalRandomSeed 变量就是 Thread 类中的一个普通 long 类型的变量,不是原子类型变量。
@sun.misc.Contended("tlr") long threadLocalRandomSeed;

因为这个变量是线程级别的,根本不需要使用原子类型变量。
ThreadLocalRandom 中的 seeder 和 probeGenerator 是两个原子性变量,将 probeGenerator 和 seeder 声明为原子变量的目的是为了在多线程情况下,赋予它们各自不同的种子初始值,这样就不会导致每个线程产生的随机数序列都是一样的,而且 probeGenerator 和 seeder 只会在初始化在初始化调用线程的种子和探针变量(用于分散计算数组索引下标)时候用到,每个线程只会使用一次。
另外变量 instance 是 ThreadLocalRandom 的一个实例,该变量是 static,多个线程使用的实例是同一个,但是由于具体的种子存在在线程里面的,所以在 ThreadlocalRandom 的实例里面只包含线程无关的的通用算法,因此它是线程安全的。
下面来看一下 ThreadLocalRandom 类的主要代码逻辑:
1.Unsafe 机制
// Unsafe mechanics 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 = 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() { if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0) localInit(); 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); }

在上面代码中,如果当前线程中的 threadLocalRandomProbe 的变量值为 0(默认情况下线程的这个变量为 0),则说明当前线程是第一次调用current()方法,那么需要调用localInit()方法计算当前线程的初始化种子变量。
这里为了延迟初始化,在不需要随机数功能的时候就不初始化 Thread 类中的种子变量。
localInit()中,首先根据 probeGenerator 计算当前线程中的 threadLocalRandomProbe 初始化值,然后根据 seeder 计算当前线程的初始化种子,然后把这两个变量设置到当前线程中。
在 current 最后返回 ThreadLocalRandom 的实例。需要注意的是,这个方法是静态方法,多个线程返回的是同一个 ThreadLocalRandom 实例。
3.int nextInt(int bound)方法
计算当前线程的下一个随机数。
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; }

上面逻辑和 Random 类似,重点是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)后去获取当前线程中 threadLocalRandomSeed 变量的值,然后在种子的基础上累加GAMMA值作为新的种子,之后使用UNSAFE的putLong方法把新的种子放入当前线程 threadLocalRandomSeed 变量中。
4.long initialSeed()方法
private static final AtomicLong seeder = new AtomicLong(initialSeed()); private static long initialSeed() { String sec = VM.getSavedProperty("java.util.secureRandomSeed"); if (Boolean.parseBoolean(sec)) { byte[] seedBytes = java.security.SecureRandom.getSeed(8); long s = (long)(seedBytes[0]) & 0xffL; for (int i = 1; i < 8; ++i) s = (s << 8) | ((long)(seedBytes[i]) & 0xffL); return s; } return (mix64(System.currentTimeMillis()) ^ mix64(System.nanoTime())); }

在初始化种子变量的初始值对应的原子变量 seeder 时,调用了initialSeed()方法,首先判断java.util.secureRandomSeed的系统属性值是否为 true 来判断是否使用安全性高的种子,如果为 true 则使用java.security.SecureRandom.getSeed(8)获取高安全性种子,如果为 false 则根据当前时间戳来获取初始化种子,也就是说使用安全性高的种子是无法被预测的,而 Random、ThreadLocalRandom 产生的被称为“伪随机数”,因为是可被预测的。
总结 ThreadLocalRandom 使用 ThreadLocal 的原理,让每个线程都持有一个本地的种子变量,该种子变量只有在使用随机数时才会被初始化。在多线程下计算新种子时是根据自己线程内维护的种子变量进行更新,从而避免了竞争。

    推荐阅读