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. cxgrid动态显示行号

    uses cxLookAndFeelPainters; type TMyCxGrid = class(TObject)    class procedure DrawIndicatorCell(    ...

  2. Maven发布和管理项目

    1 什么是Maven? 如果没有Maven,你可能不得不经历下面的过程: 1 如果使用了spring,去spring的官网下载jar包:如果使用hibernate,去hibernate的官网下载Jar ...

  3. LeetCode136:Single Number

    题目: Given an array of integers, every element appears twice except for one. Find that single one. No ...

  4. Asp.net Core 和类库读取配置文件信息

    Asp.net Core 和类库读取配置文件信息 看干货请移步至.net core 读取配置文件公共类 首先开一个脑洞,Asp.net core 被使用这么长时间了,但是关于配置文件(json)的读取 ...

  5. WPF CompositionTarget

    CompositionTarget 是一个类,表示其绘制你的应用程序的显示图面. WPF 动画引擎提供了许多用于创建基于帧的动画的功能. 但是,有应用程序方案中,您需要通过基于每个帧来呈现控件. Co ...

  6. c# 锁 Interlocked 操作

    //定义原子变量 ; //原子级别+1值,如果>=0,说明当前锁为空,可以执行,避免重复执行 ) { if (_serverThread == null || (_serverThread.Is ...

  7. LINQ to SQL语句大全

    LINQ to SQL语句大全     LINQ to SQL语句(1)之Where 适用场景:实现过滤,查询等功能. 说明:与SQL命令中的Where作用相似,都是起到范围限定也就是过滤作用的,而判 ...

  8. POI2014 RAJ-Rally

    Description 给定一个\(N\)个点\(M\)条边的\(DAG(N,M\leq10^6)\),边权为\(1\).删去一个点,使剩余图中的最长路径最短,求删去的点和最长路径长度. Soluti ...

  9. 双向链表的实现——java

    实现类: /** * Java 实现的双向链表. * 注:java自带的集合包中有实现双向链表,路径是:java.util.LinkedList * * @author skywang * @date ...

  10. PyMysql复习

    参考:http://www.cnblogs.com/liwenzhou/p/8032238.html 使用pycharm操作数据库. 填一个数据库名,User:填root 填写要连接的数据库. 建表. ...