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. selenium自动化测试时,chrome 出现“Chrome 正受到自动测试软件的控制”的解决办法

    背景 使用selenium自动化测试的时候,启动浏览器出现'Chrome正在受到自动软件的控制'的问题,修改方法有两种. 一.在浏览器配置里加个参数,忽略掉这个警告提示语:disable_infoba ...

  2. 调整WebBrowser的默认浏览器内核版本

    原文出自:https://my.oschina.net/Tsybius2014/blog/492107 注:这个是写.net控件,其实delphi是一样的.作者已经写的比较全面了,我只是做了一点修改 ...

  3. 文件上传&&验证文件格式

    $(function(){ $(".layui-progress").hide(); $("[data-upload-file]").each(function ...

  4. Prometheus 详解

    Prometheus 章节 1.Prometheus 简介 2.Prometheus 安装与配置 3.Exporter 4.Pushgateway 5.本地存储和远程存储 6.高可用方案 7.报警插件 ...

  5. 将excle表中得数据生成insert语句插入到数据库中

    第一步:输入公式 第二步:拽住右下角得+往下拖拽

  6. Uva10817_Headmaster's Headache

    大致题意就是: 一个学校招聘人,自带老师m个,n个求职的人,需要讲授s个课程,已经知道了每个人工资,问怎么才能让各科至少有两个老师(自带的必须要) 这题刚看的时候大概知道是要状态转移,可问题是,状态转 ...

  7. Ubuntu apt-get 更新/查看软件

    ubuntu 升级软件: sudo apt-get update 更新源 sudo apt-get upgrade 更新已安装的包 sudo apt-get dist-upgrade 升级系统 ubu ...

  8. strcoll - 用当前的区域选项来比较两个字符串

    总览 (SYNOPSIS) #include <string.h> int strcoll(const char *s1, const char *s2); 描述 (DESCRIPTION ...

  9. HBase之BlockCache数据读取(转)

    转自:http://blog.csdn.net/u014297175/article/details/47976909 Hbase上Regionserver的内存分为两个部分,一部分作为Memstor ...

  10. 决策树(Decision Tree)算法 python简单实现

    "" """ import numpy as np from math import log import operator import json ...