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. PicoCTF 2013 Dark Star 分析

    0x00题目 题目可以从GitHub中找到:https://github.com/picoCTF/2013-Problems/blob/master/Dark%20Star/darkstar.img ...

  2. 用iptables实现代理上网

    环境:内网:eth1:192.168.2.0/24外网:eth0:10.17.0.111用iptables实现NATSNAT:改变数据包的源地址.防火墙会使用外部地址,替换数据包的本地网络地址.这样使 ...

  3. Eclipes 配置src.zip(查看源代码)

    接着将这些改变应用,重启eclipes即可.

  4. shell 删除除匹配字符串之外的所有文件夹

    file_dir=` -maxdepth - type d`for dir in $file_dirdo file_name=`basename $dir` if [ $file_name != &q ...

  5. 转 MySQL数据库面试问题集锦

    如何设计一个高并发的系统 ① 数据库的优化,包括合理的事务隔离级别.SQL语句优化.索引的优化 ② 使用缓存,尽量减少数据库 IO ③ 分布式数据库.分布式缓存 ④ 服务器的负载均衡 锁的优化策略 ① ...

  6. sql server日志传送实践(基于server 2008 R2)

    SQL Server 2008 R2 主从数据库同步 相关参考:http://blog.itpub.net/30126024/viewspace-2639526/ sql server日志传送(基于s ...

  7. ZanUI-WeApp -- 一个颜值高、好用、易扩展的微信小程序 UI 库

    ZanUI-WeApp -- 一个颜值高.好用.易扩展的微信小程序 UI 库:https://cnodejs.org/topic/589d625a5c8036f7019e7a4a 微信小程序之官方UI ...

  8. 数据概览神器pandas_profiling

    安装: pip install pandas_profiling 用法如下: import pandas as pd import pandas_profiling df = pd.read_exce ...

  9. Python科学计算:用NumPy快速处理数据

    创建数组 import numpy as np a=np.array([1,2,3]) b=np.array([[1,2,3],[4,5,6],[7,8,9]]) b[1,1]=10 print(a. ...

  10. Linux利器 strace [看出process呼叫哪個system call]

    Linux利器 strace strace常用来跟踪进程执行时的系统调用和所接收的信号. 在Linux世界,进程不能直接访问硬件设备,当进程需要访问硬件设备(比如读取磁盘文件,接收网络数据等等)时,必 ...