JUC源码分析-其它工具类(一)ThreadLocalRandom

ThreadLocalRandom 是 JDK7 在 JUC 包下新增的随机数生成器,它解决了 Random 在多线程下多个线程竞争内部唯一的原子性种子变量而导致大量线程自旋重试的不足。需要注意的是 Random 本身是线程安全的。同时 Random 实例不是安全可靠的加密,可以使用 java.security.SecureRandom 来提供一个可靠的加密。

1. 随机数算法介绍

常用的随机数算法有两种:同余法(Congruential method)和梅森旋转算法(Mersenne twister)。Random 类中用的就是同余法中的一种 - 线性同余法(见Donald Kunth《计算机编程的艺术》第二卷,章节3.2.1)。

在程序中为了使表达式的结果小于某个值,常常采用取余的操作,结果是同一个除数的余数,这种方法叫同余法(Congruential method)。

线性同余法是一个很古老的随机数生成算法,它的数学形式如下:

Xn+1 = (a * Xn + c) % m

其中,m > 0, 0 < a < m, 0 < c < m

从 java 源码看随机数生成算法 - 线性同余算法

2. Random 源码分析

JDK 中的 Random 类生成的是伪随机数,使用的是 48-bit 的种子,然后调用线性同余方程,代码很简洁。

2.1 数据结构

private static final long multiplier = 0x5DEECE66DL;    // 相当于上面表达式中的 a
private static final long mask = (1L << 48) - 1; // 相当于上面表达式中的 m
private static final long addend = 0xBL; // 相当于上面表达式中的 c // seed 生成的随机数种子
private final AtomicLong seed;

2.2 构造函数

// ^ 让 seed 更加随机
public Random() {
this(seedUniquifier() ^ System.nanoTime());
}
public Random(long seed) {
if (getClass() == Random.class)
// initialScramble 初始化的随机数
this.seed = new AtomicLong(initialScramble(seed));
else {
this.seed = new AtomicLong(); // 子类重写 setSeed
setSeed(seed);
}
} // 不太明白,不过也不影响代码阅读
private static final AtomicLong seedUniquifier = new AtomicLong(8682522807148012L);
private static long seedUniquifier() {
for (;;) {
long current = seedUniquifier.get();
long next = current * 181783497276652981L;
if (seedUniquifier.compareAndSet(current, next))
return next;
}
} // 初始化的随机数
private static long initialScramble(long seed) {
return (seed ^ multiplier) & mask;
}

构造函数初始化了随机数种子 seed,之后的随机数都是在这个基础上进行计算的。 如果传入的 seed 值一样,那么生成的随机数也就是一样的了。

@Test
public void test() {
long seed = 343L;
Random random1 = new Random(seed);
Random random2 = new Random(seed); Assert.assertEquals(random1.nextInt(), random2.nextInt());
Assert.assertEquals(random1.nextInt(), random2.nextInt());
Assert.assertEquals(random1.nextInt(), random2.nextInt());
}

2.3 生成随机数

public int nextInt() {
return next(32);
}
public int nextInt(int bound) {
if (bound <= 0)
throw new IllegalArgumentException(BadBound);
// 1. 生成随机数
int r = next(31);
int m = bound - 1;
// 2. 生成的随机数不能超过 bound。 (n&-n)==n 也可以判断2^n
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;
} 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));
return (int)(nextseed >>> (48 - bits));
}

可以看到上面代码可知新的随机数的生成需要两个步骤:

(1) 首先需要根据老的种子生成新的种子。

(2) 然后根据新的种子来计算新的随机数。

3. ThreadLocalRandom 源码分析

为了解决多线程高并发下 Random 的缺陷,JUC 包下新增了 ThreadLocalRandom 类。更多参考并发包中ThreadLocalRandom类原理剖析

3.1 ThreadLocalRandom 原理

@Test
public void testThreadLocalRandom() {
ThreadLocalRandom random = ThreadLocalRandom.current();
System.out.println(random.nextInt());
}

从名字看会让我们联想到基础篇讲解的 ThreadLocal,ThreadLocal 的出现就是为了解决多线程访问一个变量时候需要进行同步的问题,让每一个线程拷贝一份变量,每个线程对变量进行操作时候实际是操作自己本地内存里面的拷贝,从而避免了对共享变量进行同步。

实际上 ThreadLocalRandom 的实现也是这个原理,Random 的缺点是多个线程会使用同一个原子性种子变量,会导致对原子变量更新的竞争。那么如果每个线程维护自己的一个种子变量,每个线程生成随机数时候根据自己老的种子计算新的种子,并使用新种子更新老的种子,然后根据新种子计算随机数,就不会存在竞争问题。这会大大提高并发性能,如下图 ThreadLocalRandom 原理:

3.2 数据结构

从 ThreadLocalRandom 类图中可以看到和 Random 保存一份 seed 不同,ThreadLocalRandom 的种子变量保存在 Thread.threadLocalRandomSeed 变量中,通过 Unsafe 操作这个变量。关于 threadLocalRandomSeed、threadLocalRandomProbe、threadLocalRandomSecondarySeed 这三个变量,Thread 类有相关的注释:

/** The current seed for a ThreadLocalRandom */
// 1. 和 Random 中的 seed 类似
long threadLocalRandomSeed; /** Probe hash value; nonzero if threadLocalRandomSeed initialized */
// 2. 在 CurrentHashMap 中有使用。probe 是探测的意思,
int threadLocalRandomProbe; /** Secondary seed isolated from public ThreadLocalRandom sequence */
int threadLocalRandomSecondarySeed;

需要注意的是这三个值都不能为 0,因为 0 在 ThreadLocalRandom 中有特殊的含义,表示还未初始化,调用 localInit() 进行初始化。

3.3 构造函数

boolean initialized;
private ThreadLocalRandom() {
initialized = true; // false during super() call
}
public static ThreadLocalRandom current() {
if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
localInit();
return instance;
}

ThreadLocalRandom 构造函数为私有的,只能通过 current 方法构建,如果 PROBE 还是默认值 0 表示未初始化,调用 localInit 进行初始化。

3.4 生成随机数 nextInt

// Random 一样也有两步:一是根据老的种子生成新的种子;
// 二是根据新的种子来计算新的随机数
public int nextInt() {
return mix32(nextSeed());
} public int nextInt(int bound) {
if (bound <= 0)
throw new IllegalArgumentException(BadBound);
int r = mix32(nextSeed());
// 1. 生成随机数
int m = bound - 1;
// 2. 生成的随机数不能超过 bound
// 2.1 bound 是 z^n 则直接取余
if ((bound & m) == 0) // power of two
r &= m;
// 2.2 没看明白,但肯定是取 [0, bound] 之间的数
else { // reject over-represented candidates
for (int u = r >>> 1; u + m - (r = u % bound) < 0; u = mix32(nextSeed()) >>> 1)
;
}
return r;
}

ThreadLocalRandom 和 Random 一样也有两步:

(1) 根据老的种子生成新的种子;

(2) 根据新的种子来计算新的随机数。

nextInt(int bound) 和 nextInt 的思路是一样的,先调用 mix32(nextSeed()) 函数生成随机数(int类型的范围),再对参数 n 进行判断,如果 n 恰好为 2 的方幂,那么直接移位就可以得到想要的结果;如果不是 2 的方幂,那么就关于 n 取余,最终使结果在[0,n)范围内。另外,for 循环语句的目的应该是防止结果为负数。

当bound为2n2n时, bound与生成的随机数相乘, 相当于取随机数的前log2boundlog2⁡bound

其它情况时, 将int的取值范围231−1231−1以bound为区间范围划分为n组, 最后一个区间的数不够bound个, 如果生成的随机数是从这个区间内生成的, 则难以保证随机性, 故需要重新生成.

// 生成新的种子,保存在 Thread.threadLocalRandomSeed 中。 GAMMA=0x9e3779b97f4a7c15L
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;
}
// 根据新种子生成随机数,随机数算法和 Random 一样了
private static int mix32(long z) {
z = (z ^ (z >>> 33)) * 0xff51afd7ed558ccdL;
return (int)(((z ^ (z >>> 33)) * 0xc4ceb9fe1a85ec53L) >>> 32);
}

3.5 其它方法

(1) getProbe

getProbe 用法参考 ConcurrentHashMap#fullAddCount 方法。

static final int getProbe() {
return UNSAFE.getInt(Thread.currentThread(), PROBE);
} static final int advanceProbe(int probe) {
probe ^= probe << 13; // 异或位运算。 xorshift
probe ^= probe >>> 17;
probe ^= probe << 5;
UNSAFE.putInt(Thread.currentThread(), PROBE, probe);
return probe;
}

(2) nextSecondarySeed

    static final int nextSecondarySeed() {
int r;
Thread t = Thread.currentThread();
if ((r = UNSAFE.getInt(t, SECONDARY)) != 0) {
r ^= r << 13; // xorshift
r ^= r >>> 17;
r ^= r << 5;
}
else {
localInit();
if ((r = (int)UNSAFE.getLong(t, SEED)) == 0)
r = 1; // avoid zero
}
UNSAFE.putInt(t, SECONDARY, r);
return r;
}

参考:

  1. 并发包中ThreadLocalRandom类原理剖析
  2. 《ThreadLocalRandom和Random性能测试》:http://www.importnew.com/12460.html
  3. 《Java中的random函数是如何实现的》:https://my.oschina.net/hosee/blog/600392
  4. 《解密随机数生成器》:https://blog.csdn.net/lihui126/article/details/46236109

每天用心记录一点点。内容也许不重要,但习惯很重要!

JUC源码分析-其它工具类(一)ThreadLocalRandom的更多相关文章

  1. JUC源码分析-集合篇:并发类容器介绍

    JUC源码分析-集合篇:并发类容器介绍 同步类容器是 线程安全 的,如 Vector.HashTable 等容器的同步功能都是由 Collections.synchronizedMap 等工厂方法去创 ...

  2. JUC源码分析-线程池篇(二)FutureTask

    JUC源码分析-线程池篇(二)FutureTask JDK5 之后提供了 Callable 和 Future 接口,通过它们就可以在任务执行完毕之后得到任务的执行结果.本文从源代码角度分析下具体的实现 ...

  3. JUC源码分析-线程池篇(一):ThreadPoolExecutor

    JUC源码分析-线程池篇(一):ThreadPoolExecutor Java 中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池.在开发过程中,合理地使用线程池 ...

  4. JUC源码分析-集合篇(十)LinkedTransferQueue

    JUC源码分析-集合篇(十)LinkedTransferQueue LinkedTransferQueue(LTQ) 相比 BlockingQueue 更进一步,生产者会一直阻塞直到所添加到队列的元素 ...

  5. JUC源码分析-集合篇(八)DelayQueue

    JUC源码分析-集合篇(八)DelayQueue DelayQueue 是一个支持延时获取元素的无界阻塞队列.队列使用 PriorityQueue 来实现. 队列中的元素必须实现 Delayed 接口 ...

  6. 鸿蒙内核源码分析(构建工具篇) | 顺瓜摸藤调试鸿蒙构建过程 | 百篇博客分析OpenHarmony源码 | v59.01

    百篇博客系列篇.本篇为: v59.xx 鸿蒙内核源码分析(构建工具篇) | 顺瓜摸藤调试鸿蒙构建过程 | 51.c.h.o 编译构建相关篇为: v50.xx 鸿蒙内核源码分析(编译环境篇) | 编译鸿 ...

  7. JUC源码分析-线程池篇(三)ScheduledThreadPoolExecutor

    JUC源码分析-线程池篇(三)ScheduledThreadPoolExecutor ScheduledThreadPoolExecutor 继承自 ThreadPoolExecutor.它主要用来在 ...

  8. JUC源码分析-线程池篇(三)Timer

    JUC源码分析-线程池篇(三)Timer Timer 是 java.util 包提供的一个定时任务调度器,在主线程之外起一个单独的线程执行指定的计划任务,可以指定执行一次或者反复执行多次. 1. Ti ...

  9. JUC源码分析-集合篇(九)SynchronousQueue

    JUC源码分析-集合篇(九)SynchronousQueue SynchronousQueue 是一个同步阻塞队列,它的每个插入操作都要等待其他线程相应的移除操作,反之亦然.SynchronousQu ...

随机推荐

  1. Autofac框架详解 转载https://www.cnblogs.com/lenmom/p/9081658.html

    一.组件 创建出来的对象需要从组件中来获取,组件的创建有如下4种(延续第一篇的Demo,仅仅变动所贴出的代码)方式: 1.类型创建RegisterType AutoFac能够通过反射检查一个类型,选择 ...

  2. jekyll介绍安装.github静态页面工具

    jekyll build # => 当前文件夹中的内容将会生成到 ./site 文件夹中. $ jekyll build --destination <destination> # ...

  3. linux与Windows下的heap

    Windows提供Heap相关的API,可以创建多个Heap. 但是Linux下只有一个意义上的Heap,就是Data Segment,由brk/sbrk系统调用来调整其大小. 参考:http://m ...

  4. pandas 使用出现的问题汇总

    问题1:<bound method NDFrame.head of 刚开始还以为是自己的数据集有问题,怎么显示不对呢! 解决: 仔细观察,是自己给的输出有问题,data.head什么鬼??? 正 ...

  5. UVA11572_Unique Snowflakes

    超级经典的题目,扫描区间,滑动窗口 对这题目的最大感受就是,单独看这个题目,其实不难,但是很多我感觉挺难或者没做出来的题目,都是由这些若干个经典的算法组合而成的 滑动窗口便是一个典型的例子!!!!遇到 ...

  6. 使用Emacs来编程

    使用Emacs来编程 */--> code {color: #FF0000} pre.src {background-color: #002b36; color: #839496;} code ...

  7. ionic3 emoj表情包插件 emoji-picker

    1.效果演示: 2.安装扩展包依赖 npm i @ionic-tools/emoji-picker --save 3.app.module.ts中导入插件 import { EmojiPickerMo ...

  8. termios, tcgetattr, tcsetattr, tcsendbreak, tcdrain, tcflush, tcflow, cfmakeraw, cfgetospeed, cfgetispeed, cfsetispeed, cfsetospeed - 获取和设置终端属性,行控制,获取和设置波特率

    SYNOPSIS 总览 #include <termios.h> #include <unistd.h> int tcgetattr(int fd, struct termio ...

  9. ARM发展简史

    ARM公司既不生产芯片也不销售芯片,它只出售芯片技术授权.却做到了在手持设备市场上占有90%以上的份额. 软银在2016年耗资320亿美元拿下ARM,使得本来就大红大紫的ARM公司,再一次窜到了业界人 ...

  10. 【Luogu】【关卡2-14】 树形数据结构(2017年10月)【AK】

    任务说明:由一个根节点分叉,越分越多,就成了树.树可以表示数据之间的从属关系 P1087 FBI树 给一个01字符串,0对应B,1对应I,F对应既有0子节点又有1子节点的根节点,输出这棵树的后序遍历. ...