小强最近面试又翻车了,然而令他郁闷的是,这次竟然是栽到了自己经常在用的 Random 上......
面试问题
既然已经有了 Random 为什么还需要 ThreadLocalRandom?
正文
Random 是使用最广泛的随机数生成工具了,即使连 Math.random() 的底层也是用 Random 实现的Math.random() 源码如下:
可以看出 Math.random() 直接指向了 Random.nextDouble() 方法。
Random 使用
这开始之前,我们先来了解一下 Random 的使用。
Random random = new Random(); for (int i = 0; i < 3; i++) { // 生成 0-9 的随机整数 random.nextInt(10); }
以上程序的执行结果为:
1
0
7
Random 源码解析
可以看出 Random 是通过 nextInt() 方法生成随机整数的,那他的底层的是如何实现的呢?我们来看他的实现源码:
/** * 源码版本:JDK 11 */ 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; }
从以上源码我们可以看出,整个源码最核心的部分有两块:
- 根据老种子生成新种子;
- 根据新种子计算出随机数。
根据新种子计算出随机数的代码已经很明确了,我们需要确认一下 next()
方法是如何实现的,继续看源码:
/** * 源码版本:JDK 11 */ protected int next(int bits) { // 声明老种子和新种子 long oldseed, nextseed; AtomicLong seed = this.seed; do { // 获取原子变量种子的值 oldseed = seed.get(); // 根据当前种子计算出新种子的值 nextseed = (oldseed * multiplier + addend) & mask; } while (!seed.compareAndSet(oldseed, nextseed)); // 使用 CAS 更新种子 return (int)(nextseed >>> (48 - bits)); }
根据以上源码可以看出,在使用老种子去获取新种子的时候,如果是多线程操作,则同一时刻只会有一个线程 CAS (Conmpare And Swap,比较并交换) 成功,其他失败的线程会通过自旋等待获取新种子,因此会有一定的性能消耗。
当多线程使用同一个老种子来 CAS 的时候,只能有一个线程能够成功,而其他失败的线程只能通过自旋等待,这也是为什么 JDK 1.7 会引入 ThreadLocalRandom 的答案了,它主要为了提升多线程情况下 Random 的执行效率。
ThreadLocalRandom 使用
我们先来看 ThreadLocalRandom 的类关系图:
可以看出 ThreadLocalRandom 继承于 Random 类,先来看它的使用:
ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current(); for (int i = 0; i < 3; i++) { // 生成 0-9 的随机数 System.out.println(threadLocalRandom.nextInt(10)); }
以上程序的执行结果为:
1
7
5
可以看出 ThreadLocalRandom 和 Random 一样,都是通过 nextInt() 方法实现随机整数生成的。