Random 类及其局限性
在 JDK 7 之前包括现在,java.util.Random 都是使用比较广泛的随机数生成工具类,而且 java.lang.Math 中的随机数生成也使用的是 java.util.Random 的实例。下面先看看 java. util.Random 的使用方法。
public class RandomTest {
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.println(random.nextInt(5));
}
}
}
代码(1)创建一个默认随机数生成器,并使用默认的种子。
代码(2)输出 10 个在 0~5(包含 0,不包含 5)之间的随机数。
随机数的生成需要一个默认的种子,这个种子其实是一个 long 类型的数字,你可以在创建 Random 对象时通过构造函数指定,如果不指定则在默认构造函数内部生成一个默认的值。有了默认的种子后,如何生成随机数呢?
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)=aseed+b;步骤(5)也可以抽象为 g(seed, bound),其中 g 是一个固定的函数,比如 g(seed, bound)=(int)((bound(long)seed)>> 31)。在单线程情况下每次调用 nextInt 都是根据老的种子计算出新的种子,这是可以保证随机数产生的随机性的。但是在多线程下多个线程可能都拿同一个老的种子去执行步骤(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));
}
代码(6)获取当前原子变量种子的值。
代码(7)根据当前种子值计算新的种子。
代码(8)使用 CAS 操作,它使用新的种子去更新老的种子,在多线程下可能多个线程都同时执行到了代码(6),那么可能多个线程拿到的当前种子的值是同一个,然后执行步骤(7)计算的新种子也都是一样的,但是步骤(8)的 CAS 操作会保证只有一个线程可以更新老的种子为新的,失败的线程会通过循环重新获取更新后的种子作为当前种子去计算老的种子,这就解决了上面提到的问题,保证了随机数的随机性。
代码(9)使用固定算法根据新的种子计算随机数。
总结:每个 Random 实例里面都有一个原子性的种子变量用来记录当前的种子值,当要生成新的随机数时需要根据当前种子计算新的种子并更新回原子变量。在多线程下使用单个 Random 实例生成随机数时,当多个线程同时计算随机数来计算新的种子时,多个线程会竞争同一个原子变量的更新操作,由于原子变量的更新是 CAS 操作,同时只有一个线程会成功,所以会造成大量线程进行自旋重试,这会降低并发性能,所以 ThreadLocalRandom 应运而生。
ThreadLocalRandom
为了弥补多线程高并发情况下 Random 的缺陷,在 JUC 包下新增了 ThreadLocalRandom 类。下面首先看下如何使用它。
public class RandomTest {
public static void main(String[] args) {
//(10)获取一个随机数生成器
ThreadLocalRandom random = ThreadLocalRandom.current();
//(11)输出 10 个在 0~5(包含 0,不包含 5)之间的随机数
for (int i = 0; i < 10; ++i) {
System.out.println(random.nextInt(5));
}
}
}
其中,代码(10)调用 ThreadLocalRandom.current()来获取当前线程的随机数生成器。下面来分析下 ThreadLocalRandom 的实现原理。从名字上看它会让我们联想到在基础篇中讲解的 ThreadLocal:ThreadLocal 通过让每一个线程复制一份变量,使得在每个线程对变量进行操作时实际是操作自己本地内存里面的副本,从而避免了对共享变量进行同步。实际上 ThreadLocalRandom 的实现也是这个原理,Random 的缺点是多个线程会使用同一个原子性种子变量,从而导致对原子变量更新的竞争,如图 3-1 所示。
图 3-1
那么,如果每个线程都维护一个种子变量,则每个线程生成随机数时都根据自己老的种子计算新的种子,并使用新种子更新老的种子,再根据新种子计算随机数,就不会存在竞争问题了,这会大大提高并发性能。ThreadLocalRandom 原理如图 3-2 所示。
源码分析
首先看下 ThreadLocalRandom 的类图结构,如图 3-3 所示。
图 3-3
从图中可以看出 ThreadLocalRandom 类继承了 Random 类并重写了 nextInt 方法,在 ThreadLocalRandom 类中并没有使用继承自 Random 类的原子性种子变量。在 ThreadLocalRandom 中并没有存放具体的种子,具体的种子存放在具体的调用线程的 threadLocalRandomSeed 变量里面。ThreadLocalRandom 类似于 ThreadLocal 类,就是个工具类。当线程调用 ThreadLocalRandom 的 current 方法时,ThreadLocalRandom 负责初始化调用线程的 threadLocalRandomSeed 变量,也就是初始化种子。
当调用 ThreadLocalRandom 的 nextInt 方法时,实际上是获取当前线程的 threadLocalRandomSeed 变量作为当前种子来计算新的种子,然后更新新的种子到当前线程的 threadLocalRandomSeed 变量,而后再根据新种子并使用具体算法计算随机数。这里需要注意的是,threadLocalRandomSeed 变量就是 Thread 类里面的一个普通 long 变量,它并不是原子性变量。其实道理很简单,因为这个变量是线程级别的,所以根本不需要使用原子性变量,如果你还是不理解可以思考下 ThreadLocal 的原理。
其中 seeder 和 probeGenerator 是两个原子性变量,在初始化调用线程的种子和探针变量时会用到它们,每个线程只会使用一次。
另外,变量 instance 是 ThreadLocalRandom 的一个实例,该变量是 static 的。当多线程通过 ThreadLocalRandom 的 current 方法获取 ThreadLocalRandom 的实例时,其实获取的是同一个实例。但是由于具体的种子是存放在线程里面的,所以在 ThreadLocalRandom 的实例里面只包含与线程无关的通用算法,所以它是线程安全的。
下面看看 ThreadLocalRandom 的主要代码的实现逻辑。
1.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 类里面 threadLocalRandomSecondarySeed 变量在 Thread 实例里面的偏移量,这个值在后面讲解 LongAdder 时会用到
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() {
//(12)
if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
//(13)
localInit();
//(14)
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);
}
在如上代码(12)中,如果当前线程中 threadLocalRandomProbe 的变量值为 0(默认情况下线程的这个变量值为 0),则说明当前线程是第一次调用 ThreadLocalRandom 的 current 方法,那么就需要调用 localInit 方法计算当前线程的初始化种子变量。这里为了延迟初始化,在不需要使用随机数功能时就不初始化 Thread 类中的种子变量,这是一种优化。
代码(13)首先根据 probeGenerator 计算当前线程中 threadLocalRandomProbe 的初始化值,然后根据 seeder 计算当前线程的初始化种子,而后把这两个变量设置到当前线程。代码(14)返回 ThreadLocalRandom 的实例。需要注意的是,这个方法是静态方法,多个线程返回的是同一个 ThreadLocalRandom 实例。
3.int nextInt(int bound)方法
计算当前线程的下一个随机数。
public int nextInt(int bound) {
//(15)参数校验
if (bound <= 0)
throw new IllegalArgumentException(BadBound);
//(16) 根据当前线程中的种子计算新种子
int r = mix32(nextSeed());
//(17)根据新种子和 bound 计算随机数
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; //
UNSAFE.putLong(t = Thread.currentThread(), SEED,
r = UNSAFE.getLong(t, SEED) + GAMMA);
return r;
}
在如上代码中,首先使用 r = UNSAFE.getLong(t, SEED)获取当前线程中 threadLocalRandomSeed 变量的值,然后在种子的基础上累加 GAMMA 值作为新种子,而后使用 UNSAFE 的 putLong 方法把新种子放入当前线程的 threadLocalRandomSeed 变量中。
总结
本章首先讲解了 Random 的实现原理以及 Random 在多线程下需要竞争种子原子变量更新操作的缺点,从而引出 ThreadLocalRandom 类。ThreadLocalRandom 使用 ThreadLocal 的原理,让每个线程都持有一个本地的种子变量,该种子变量只有在使用随机数时才会被初始化。在多线程下计算新种子时是根据自己线程内维护的种子变量进行更新,从而避免了竞争。