JDK 并发包中 ThreadLocalRandom 类原理剖析,经常使用的随机数生成器 Random 类的原理是什么?及其局限性是什么?ThreadLocalRandom 是如何利用 ThreadLocal 的原理来解决 Random 的局限性?

我们首先看Random 类及其局限性,如下:

  在 JDK7 之前包括现在,java.util.Random 应该是使用比较广泛的随机数生成工具类,另外 java.lang.Math 中的随机数生成也是使用的 java.util.Random 的实例。下面先看看 java.util.Random 的使用例子如下:

/**
* Created by cong on 2018/6/4.
*/
public class RandomTest { public static void main(String[] args) { //(1)创建一个默认种子的随机数生成器
Random random = new Random();
//(2)输出10个在0-5(包含0,不包含5)之间的随机数
for (int i = ; i < ; ++i) {
System.out.println(random.nextInt());
} }
}

代码(1)创建一个默认随机数生成器,使用默认的种子。

代码(2)输出输出10个在0-5(包含0,不包含5)之间的随机数。

运行结果如下:

这里提下随机数的生成需要一个默认的种子,这个种子实际上就是是一个 long 类型的数字,这个种子要么在 Random 的时候通过构造函数指定,那么默认构造函数内部会生成一个默认的值,问题来了,有了默认的种子后,如何生成随机数呢?

我们进入Radom类里面去看nextInt方法的源码,如下:

    public int nextInt(int var1) {
    //(3)参数校验
if(var1 <= ) {
throw new IllegalArgumentException("bound must be positive");
} else {
       //(4)根据老的种子生成心的种子
int var2 = this.next();
       //(5)以下根据新的种子计算随机数
int var3 = var1 - ;
if((var1 & var3) == ) {
var2 = (int)((long)var1 * (long)var2 >> );
} else {
for(int var4 = var2; var4 - (var2 = var4 % var1) + var3 < ; var4 = this.next()) {
;
}
} return var2;
}
}

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

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

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

其中步骤(4)我们可以抽象为 seed=f(seed),其中 f 是一个固定的函数,比如 seed= f(seed)=a*seed+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 >>> ( - bits));
}

代码(6)获取当前原子变量种子的值;

代码(7)根据当前种子值计算新的种子;

代码(8)使用 CAS 操作,使用新的种子去更新老的种子,多线程下可能多个线程都同时执行到了代码(6),那么可能多个线程都拿到的当前种子的值是同一个,然后执行步骤(7)计算的新种子也都是一样的,但是步骤(8)的 CAS 操作会保证只有一个线程可以更新老的种子为新的,

失败的线程会通过循环重新获取更新后的种子作为当前种子去计算老的种子,可见这里解决了上面提到的问题,也就保证了随机数的随机性。

代码(9)则使用固定算法根据新的种子计算随机数。

因此,每个 Random 实例里面有一个原子性的种子变量用来记录当前的种子的值,当要生成新的随机数时候要根据当前种子计算新的种子并更新回原子变量。多线程下使用单个 Random 实例生成随机数时候,多个线程同时计算新的种子时候会竞争同一个原子变量的更新操作,、

由于原子变量的更新是 CAS 操作,同时只有一个线程会成功,所以会造成大量线程进行自旋重试,这是会降低并发性能的,所以 ThreadLocalRandom 应运而生。

为了解决多线程高并发下 Random 的缺陷,JUC 包下新增了 ThreadLocalRandom 类。我首先先看 ThreadLocalRandom的原理,如下图:

接着我们再看一下ThreadLocalRandom 的类图结构,如下图:

可知 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 的主要代码实现逻辑,如下:

首先是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类里面threadLocalRandomProbe变量在Thread实例里面偏移量,这个值在后面讲解的LongAdder里面会用到
SECONDARY = UNSAFE.objectFieldOffset
(tk.getDeclaredField("threadLocalRandomSecondarySeed"));
} catch (Exception e) {
throw new Error(e);
}
}

ThreadLocalRandom current() 方法:该方法获取 ThreadLocalRandom 实例,并初始化调用线程中 threadLocalRandomSeed 和 threadLocalRandomProbe 变量。源码如下:

  static final ThreadLocalRandom instance = new ThreadLocalRandom();
public static ThreadLocalRandom current() {
//(12)
if (UNSAFE.getInt(Thread.currentThread(), PROBE) == )
//(13)
localInit();
//(14)
return instance;
}
static final void localInit() {
int p = probeGenerator.addAndGet(PROBE_INCREMENT);
int probe = (p == ) ? : 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 实例。

int nextInt(int bound) 方法:计算当前线程的下一个随机数。源码如下图所示:

 public int nextInt(int bound) {
//(15)参数校验
if (bound <= )
throw new IllegalArgumentException(BadBound);
//(16) 根据当前线程中种子计算新种子
int r = mix32(nextSeed());
//(17)根据新种子和bound计算随机数
int m = bound - ;
if ((bound & m) == ) // power of two
r &= m;
else { // reject over-represented candidates
for (int u = r >>> ;
u + m - (r = u % bound) < ;
u = mix32(nextSeed()) >>> )
;
}
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 变量。

理论知道了,那么现在用一个例子来讲解 ThreadLocalRandom 如何使用,例子如下:

public class RandomTest {

    public static void main(String[] args) {
//(10)获取一个随机数生成器
ThreadLocalRandom random = ThreadLocalRandom.current(); //(11)输出10个在0-5(包含0,不包含5)之间的随机数
for (int i = ; i < ; ++i) {
System.out.println(random.nextInt());
}
}
}

运行结果如下:

如上代码(10)调用 ThreadLocalRandom.current() 来获取当前线程的随机数生成器。

ThreadLocal 的出现就是为了解决多线程下变量的隔离问题,让每一个线程拷贝一份变量,每个线程对变量进行操作时候实际是操作自己本地内存里面的拷贝。

实际上 ThreadLocalRandom 的实现也是这个原理,Random 的缺点是多个线程会使用原子性种子变量,会导致对原子变量更新的竞争,如下图:

如果每个线程维护自己的一个种子变量,每个线程生成随机数时候根据自己老的种子计算新的种子,并使用新种子更新老的种子,然后根据新种子计算随机数,就不会存在竞争问题,这会大大提高并发性能。这就是ThreadLocalRandom使用ThreadLocal的原理的独到之处。

到目前为止,我们知道了 Random 的实现原理以及介绍了 Random 在多线程下存在竞争种子原子变量更新操作失败后自旋等待的缺点,从而引出 ThreadLocalRandom 类,ThreadLocalRandom 使用 ThreadLocal 的原理,让每个线程内持有一个本地的种子变量,

该种子变量只有在使用随机数时候才会被初始化,多线程下计算新种子时候是根据自己线程内维护的种子变量进行更新,从而避免了竞争。

Java并发编程笔记之ThreadLocalRandom源码分析的更多相关文章

  1. Java并发编程笔记之CopyOnWriteArrayList源码分析

    并发包中并发List只有CopyOnWriteArrayList这一个,CopyOnWriteArrayList是一个线程安全的ArrayList,对其进行修改操作和元素迭代操作都是在底层创建一个拷贝 ...

  2. Java并发编程笔记之ThreadLocal源码分析

    多线程的线程安全问题是微妙而且出乎意料的,因为在没有进行适当同步的情况下多线程中各个操作的顺序是不可预期的,多线程访问同一个共享变量特别容易出现并发问题,特别是多个线程需要对一个共享变量进行写入时候, ...

  3. Java并发编程笔记之FutureTask源码分析

    FutureTask可用于异步获取执行结果或取消执行任务的场景.通过传入Runnable或者Callable的任务给FutureTask,直接调用其run方法或者放入线程池执行,之后可以在外部通过Fu ...

  4. Java并发编程笔记之SimpleDateFormat源码分析

    SimpleDateFormat 是 Java 提供的一个格式化和解析日期的工具类,日常开发中应该经常会用到,但是由于它是线程不安全的,多线程公用一个 SimpleDateFormat 实例对日期进行 ...

  5. Java并发编程笔记之Timer源码分析

    timer在JDK里面,是很早的一个API了.具有延时的,并具有周期性的任务,在newScheduledThreadPool出来之前我们一般会用Timer和TimerTask来做,但是Timer存在一 ...

  6. Java并发编程笔记之CyclicBarrier源码分析

    JUC 中 回环屏障 CyclicBarrier 的使用与分析,它也可以实现像 CountDownLatch 一样让一组线程全部到达一个状态后再全部同时执行,但是 CyclicBarrier 可以被复 ...

  7. Java并发编程笔记之PriorityBlockingQueue源码分析

    JDK 中无界优先级队列PriorityBlockingQueue 内部使用堆算法保证每次出队都是优先级最高的元素,元素入队时候是如何建堆的,元素出队后如何调整堆的平衡的? PriorityBlock ...

  8. Java并发编程笔记之ArrayBlockingQueue源码分析

    JDK 中基于数组的阻塞队列 ArrayBlockingQueue 原理剖析,ArrayBlockingQueue 内部如何基于一把独占锁以及对应的两个条件变量实现出入队操作的线程安全? 首先我们先大 ...

  9. Java并发编程笔记之ReentrantLock源码分析

    ReentrantLock是可重入的独占锁,同时只能有一个线程可以获取该锁,其他获取该锁的线程会被阻塞后放入该锁的AQS阻塞队列里面. 首先我们先看一下ReentrantLock的类图结构,如下图所示 ...

随机推荐

  1. .Net socket服务器编程之为何也高效

    说到Socket编程,肯定大部分人举手c,c++.可惜现在已没有机会去追随并达到写服务器的水平,所以将就下还是考虑c#版的Socket服务器吧. 经过一番查询,试用.一些数据和事实还是浮出水面,同时对 ...

  2. 团队项目(HCL队)第二周

    一.项目介绍 1.内容 我们队选择的题目是经典90坦克大战的java实现,后续会加入ai,以实现更丰富的体验. 2.预期使用数量 原版的经典90坦克大战拥有众多粉丝,我们在其上进行拓展,目前预计用户量 ...

  3. .NET Core MemoryCache缓存获取全部缓存键

    在Core中不能使用原HttpRuntime.Cache缓存,改为MemoryCache(Microsoft.Extensions.Caching.Memory). 现MemoryCache新版为2. ...

  4. dorado7-HelloWorld

    1.首先在Tomat中将 Auto reloding enable去掉,去掉的目的不用每次更改代码,都要重新部署 2.创建dorado视图文件 2.1 视图文件的格式为xml 2.2 在view中添加 ...

  5. ASP.NET Core 2 学习笔记(三)中间件

    之前ASP.NET中使用的HTTP Modules及HTTP Handlers,在ASP.NET Core中已不复存在,取而代之的是Middleware.Middleware除了简化了HTTP Mod ...

  6. C#获取微信二维码显示到wpf

    微信的api开放的二维码是一个链接地址,而我们要将这个二维码显示到客户端.方式很多,今天我们讲其中一种. /// <summary> /// 获取图片路径 /// </summary ...

  7. (2)特征点匹配,并求旋转矩阵R和位移向量t

    include头文件中有slamBase.h # pragma once // 各种头文件 // C++标准库 #include <fstream> #include <vector ...

  8. PHP之旅4 php 超全局变量

    预定义数组: 自动全局变量---超全局数组 1.包含了来自web服务器,客户端,运行环境和用户输入的数据 2.这些数组比较特别 3.全局范围内自动生效,都可以直接使用这些数组 4.用户不能自定义这些数 ...

  9. 初始化css文件

    首先我们需要了解一下为什么需要公共样式(公共样式是为了初始化某些标签的默认值): 1. 因为浏览器的兼容问题,不同浏览器对有些标签的默认值是不同的,如果没对CSS初始化往往会出现浏览器之间的页面显示差 ...

  10. iOS下载图片失败

    一.具体问题 开发的过程中,发现某个界面部分图片的显示出现了问题只显示占位图片,取出图片的url在浏览器却是能打开的,各种尝试甚至找同行的朋友帮忙在他们项目里展示都会存在问题,最终发现通过第三方框架S ...