前言
ThreadLocalRandom类是JDK7在JUC包下新增的随机数生成器,它解决了Random类在多线程下多个线程竞争内部唯一的原子性种子变量而导致大量线程自旋重试的不足。
Random的局限性
java.util.Random是使用较为广泛的随机数生成工具类,另外java.lang.Math中的随机数生成也是使用的java.util.Random的实例。我们看下Random中生成随机数的源码
public int nextInt(int bound) {
//参数检查
if (bound <= 0)
throw new IllegalArgumentException(BadBound);
//根据老的种子生成新的种子
int r = next(31);
//根据新的种子计算随机数
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;
}
这里说明下,随机数的生成需要一个默认的种子,这个种子其实是一个long类型的数字,这个种子要么在Random的时候通过构造函数指定,那么默认构造函数内部会生成一个默认的值
public Random() {
this(seedUniquifier() ^ System.nanoTime());
}
private static long seedUniquifier() {
// L'Ecuyer, "Tables of Linear Congruential Generators of
// Different Sizes and Good Lattice Structure", 1999
for (;;) {
long current = seedUniquifier.get();
long next = current * 181783497276652981L;
if (seedUniquifier.compareAndSet(current, next))
return next;
}
}
在单线程情况下,每次调用nextInt方法都是根据老的种子计算出新的种子,这是可以保证随机数产生的随机性的,但是在多线程下多个线程可能都拿到同一个老的种子,去生成新的种子,那么怎么确保每一个线程拿到的老的种子不是同一个呢?这里Random函数使用了一个原子变量操作的方法达到了目的,在创建Random对象时初始化的种子被保存到了原子变量里面,下面看下next()的源码
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));
return (int)(nextseed >>> (48 - bits));
}
我们可以看到,next()方法使用了CAS操作,用新生成的种子去更新老的种子,在多线程下可能多个线程同时执行到了seed.get()方法,获得当前的种子,这样多个线程获得的种子的值就是同一个,计算得出的新的种子也会是一个,但是CAS操作保证了只有一个线程可以更新老的种子为新的。因为compareAndSet()底层也是调用Unsafe类的compareAndSwapLong()方法,直接更新的是Random对象中老种子的偏移量上的值并且成功返回true,失败返回false。那些失败的线程会通过do/while循环重新获取被更新后的种子,作为当前的种子进行运算。
总结下:每个Random实例里面有一个原子性的种子变量用来记录当前的种子的值,当要生成新的随机数时候要根据当前种子计算新的种子并更新回原子变量。多线程下使用单个Random实例生成随机数时候,多个线程同时计算随机数计算新的种子时候多个线程会竞争同一个原子变量的更新操作,由于原子变量的更新是CAS操作,同时只有一个线程会成功,所以会造成大量线程进行自旋重试,这是会降低并发性能的,所以ThreadLocalRandom应运而生
ThreadLocalRandom
为了解决多线程高并发下Random的缺陷,JUC包下新增了ThreadLocalRandom类。他的原理其实和TreadLocal的原理比较相似。Random的缺点是多个线程会使用原子性种子变量,会导致对原子变量更新的竞争,如下图:
那么如果每个线程维护自己的一个种子变量,每个线程生成随机数时候根据自己老的种子计算新的种子,并使用新种子更新老的种子,然后根据新种子计算随机数,就不会存在竞争问题,这会大大提高并发性能,如下图ThreadLocalRandom原理:
我们来看下ThreadLocalRandom的类图结构:
ThreadLocalRandom继承了Random并重写了nextInt方法,ThreadLocalRandom中并没有使用继承自Random的原子性种子变量。具体的种子是存放到具体的调用线程的threadLocalRandomSeed变量里面的,ThreadLocalRandom类似于ThreadLocal类就是个工具类。
当线程调用ThreadLocalRandom的current方法时候ThreadLocalRandom负责初始化调用线程的threadLocalRandomSeed变量,也就是初始化种子。当调用ThreadLocalRandom的nextInt方法时候,实际上是获取当前线程的threadLocalRandomSeed变量作为当前种子来计算新的种子,然后更新新的种子到当前线程的threadLocalRandomSeed变量,然后在根据新种子和具体算法计算随机数。这里需要注意的是threadLocalRandomSeed变量就是Thread类里面的一个普通long变量,是不是跟ThreadLocal的原理很像。
其中变量seeder和probeGenerator是两个原子性变量,在初始化调用线程的种子和探针变量时候用到,每个线程只会使用一次。
下面看看ThreadLocalRandom的主要实现源码
- 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类里面threadLocalRandomProbe变量在Thread实例里面偏移量
SECONDARY = UNSAFE.objectFieldOffset
(tk.getDeclaredField("threadLocalRandomSecondarySeed"));
} catch (Exception e) {
throw new Error(e);
}
}
- 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;
long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));
Thread t = Thread.currentThread();
UNSAFE.putLong(t, SEED, seed);
UNSAFE.putInt(t, PROBE, probe);
}
如果当前线程中threadLocalRandomProbe变量值为0(默认情况下线程的这个变量为0),说明当前线程第一次调用ThreadLocalRandom的current方法,那么就需要调用localInit方法计算当前线程的初始化种子变量。这里设计为了延迟初始化,不需要使用随机数功能时候Thread类中的种子变量就不需要被初始化。
首先计算根据probeGenerator计算当前线程中threadLocalRandomProbe的初始化值,然后根据seeder计算当前线程的初始化种子,然后把这两个变量设置到当前线程。返回ThreadLocalRandom的实例,这里的这个方法是静态方法,多个线程返回的是同一个ThreadLocalRandom实例。
- int nextInt(int bound)方法和nextSeed():
public int nextInt(int bound) {
if (bound <= 0)
throw new IllegalArgumentException(BadBound);
int r = mix32(nextSeed());
int m = bound - 1;
if ((bound & m) == 0)
r &= m;
else {
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;
}
nextInt()跟Random里的大致相同,主要看下nextSeed() ;使用 r = UNSAFE.getLong(t, SEED)获取当前线程中threadLocalRandomSeed变量的值,然后在种子的基础上累加GAMMA值作为新种子,然后使用UNSAFE的putLong方法把新种子放入当前线程的threadLocalRandomSeed变量。
总结一下:ThreadLocalRandom使用ThreadLocal的原理,让每个线程内持有一个本地的种子变量,该种子变量只有在使用随机数时候才会被初始化,多线程下计算新种子时候是根据自己线程内维护的种子变量进行更新,从而避免了竞争。