原文链接:https://www.jianshu.com/p/9c2198586f9b

2.2、 并发包中ThreadLocalRandom类原理剖析

ThreadLocalRandom类是JDK7在JUC包下新增的随机数生成器,它解决了Random类在多线程下多个线程竞争内部唯一的原子性种子变量而导致大量线程自旋重试的不足。本节首先讲解下Random类的实现原理已经它在多线程下使用的局限性,然后引入ThreadLocalRandom类,通过讲解ThreadLocalRandom的实现原理来说明ThreadLocalRandom是如何解决的Random类的不足。

2.2.1 Random类及其局限性

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

  1. public class RandomTest {
  2. public static void main(String[] args) {
  3.  
  4. //(1)创建一个默认种子的随机数生成器
  5. Random random = new Random();
  6. //(2)输出10个在0-5(包含0,不包含5)之间的随机数
  7. for (int i = 0; i < 10; ++i) {
  8. System.out.println(random.nextInt(5));
  9. }
  10. }
  11. }
  • 代码(1)创建一个默认随机数生成器,使用默认的种子。
  • 代码(2)输出输出10个在0-5(包含0,不包含5)之间的随机数。

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

  1. public int nextInt(int bound) {
  2. //(3)参数检查
  3. if (bound <= 0)
  4. throw new IllegalArgumentException(BadBound);
  5. //(4)根据老的种子生成新的种子
  6. int r = next(31);
  7. //(5)根据新的种子计算随机数
  8. ...
  9. return r;
  10. }

如上代码可知新的随机数的生成需要两个步骤

  • 首先需要根据老的种子生成新的种子。
  • 然后根据新的种子来计算新的随机数。

其中步骤(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()代码:

  1. protected int next(int bits) {
  2. long oldseed, nextseed;
  3. AtomicLong seed = this.seed;
  4. do {
  5. //(6)
  6. oldseed = seed.get();
  7. //(7)
  8. nextseed = (oldseed * multiplier + addend) & mask;
  9. //(8)
  10. } while (!seed.compareAndSet(oldseed, nextseed));
  11. //(9)
  12. return (int)(nextseed >>> (48 - bits));
  13. }
  • 代码(6)获取当前原子变量种子的值
  • 代码(7)根据当前种子值计算新的种子
  • 代码(8)使用CAS操作,使用新的种子去更新老的种子,多线程下可能多个线程都同时执行到了代码(6)那么可能多个线程都拿到的当前种子的值是同一个,然后执行步骤(7)计算的新种子也都是一样的,但是步骤(8)的CAS操作会保证只有一个线程可以更新老的种子为新的,失败的线程会通过循环从新获取更新后的种子作为当前种子去计算老的种子,可见这里解决了上面提到的问题,也就保证了随机数的随机性。
  • 代码(9)则使用固定算法根据新的种子计算随机数。

总结下:每个Random实例里面有一个原子性的种子变量用来记录当前的种子的值,当要生成新的随机数时候要根据当前种子计算新的种子并更新回原子变量。多线程下使用单个Random实例生成随机数时候,多个线程同时计算随机数计算新的种子时候多个线程会竞争同一个原子变量的更新操作,由于原子变量的更新是CAS操作,同时只有一个线程会成功,所以会造成大量线程进行自旋重试,这是会降低并发性能的,所以ThreadLocalRandom应运而生。

2.2.2 ThreadLocalRandom

为了解决多线程高并发下Random的缺陷,JUC包下新增了ThreadLocalRandom类,下面首先看下它如何使用:

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

如上代码(10)调用ThreadLocalRandom.current()来获取当前线程的随机数生成器。下面来分析下ThreadLocalRandom的实现原理。从名字看会让我们联想到基础篇讲解的ThreadLocal,ThreadLocal的出现就是为了解决多线程访问一个变量时候需要进行同步的问题,让每一个线程拷贝一份变量,每个线程对变量进行操作时候实际是操作自己本地内存里面的拷贝,从而避免了对共享变量进行同步。实际上ThreadLocalRandom的实现也是这个原理,Random的缺点是多个线程会使用原子性种子变量,会导致对原子变量更新的竞争,如下图:

 
image.png

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

 
image.png

2.2.3 源码分析

首先看下ThreadLocalRandom的类图结构:

 
image.png

可知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 机制的使用
  1. private static final sun.misc.Unsafe UNSAFE;
  2. private static final long SEED;
  3. private static final long PROBE;
  4. private static final long SECONDARY;
  5. static {
  6. try {
  7. //获取unsafe实例
  8. UNSAFE = sun.misc.Unsafe.getUnsafe();
  9. Class<?> tk = Thread.class;
  10. //获取Thread类里面threadLocalRandomSeed变量在Thread实例里面偏移量
  11. SEED = UNSAFE.objectFieldOffset
  12. (tk.getDeclaredField("threadLocalRandomSeed"));
  13. //获取Thread类里面threadLocalRandomProbe变量在Thread实例里面偏移量
  14. PROBE = UNSAFE.objectFieldOffset
  15. (tk.getDeclaredField("threadLocalRandomProbe"));
  16. //获取Thread类里面threadLocalRandomProbe变量在Thread实例里面偏移量
  17. SECONDARY = UNSAFE.objectFieldOffset
  18. (tk.getDeclaredField("threadLocalRandomSecondarySeed"));
  19. } catch (Exception e) {
  20. throw new Error(e);
  21. }
  22. }
  • ThreadLocalRandom current()方法:该方法获取ThreadLocalRandom实例,并初始化调用线程中threadLocalRandomSeed和threadLocalRandomProbe变量。
  1. static final ThreadLocalRandom instance = new ThreadLocalRandom();
  2. public static ThreadLocalRandom current() {
  3. //(12)
  4. if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
  5. //(13)
  6. localInit();
  7. //(14)
  8. return instance;
  9. }
  10. static final void localInit() {
  11. int p = probeGenerator.addAndGet(PROBE_INCREMENT);
  12. int probe = (p == 0) ? 1 : p; // skip 0
  13. long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));
  14. Thread t = Thread.currentThread();
  15. UNSAFE.putLong(t, SEED, seed);
  16. UNSAFE.putInt(t, PROBE, probe);
  17. }

如上代码(12)如果当前线程中threadLocalRandomProbe变量值为0(默认情况下线程的这个变量为0),说明当前线程第一次调用ThreadLocalRandom的current方法,那么就需要调用localInit方法计算当前线程的初始化种子变量。这里设计为了延迟初始化,不需要使用随机数功能时候Thread类中的种子变量就不需要被初始化,这是一种优化。

代码(13)首先计算根据probeGenerator计算当前线程中threadLocalRandomProbe的初始化值,然后根据seeder计算当前线程的初始化种子,然后把这两个变量设置到当前线程。
代码(14)返回ThreadLocalRandom的实例,需要注意的是这个方法是静态方法,多个线程返回的是同一个ThreadLocalRandom实例。

  • int nextInt(int bound)方法:计算当前线程的下一个随机数
  1. public int nextInt(int bound) {
  2. //(15)参数校验
  3. if (bound <= 0)
  4. throw new IllegalArgumentException(BadBound);
  5. //(16) 根据当前线程中种子计算新种子
  6. int r = mix32(nextSeed());
  7. //(17)根据新种子和bound计算随机数
  8. int m = bound - 1;
  9. if ((bound & m) == 0) // power of two
  10. r &= m;
  11. else { // reject over-represented candidates
  12. for (int u = r >>> 1;
  13. u + m - (r = u % bound) < 0;
  14. u = mix32(nextSeed()) >>> 1)
  15. ;
  16. }
  17. return r;
  18. }

如上代码逻辑步骤与Random相似,我们重点看下nextSeed()方法:

  1. final long nextSeed() {
  2. Thread t; long r; //
  3. UNSAFE.putLong(t = Thread.currentThread(), SEED,
  4. r = UNSAFE.getLong(t, SEED) + GAMMA);
  5. return r;
  6. }

如上代码首先使用 r = UNSAFE.getLong(t, SEED)获取当前线程中threadLocalRandomSeed变量的值,然后在种子的基础上累加GAMMA值作为新种子,然后使用UNSAFE的putLong方法把新种子放入当前线程的threadLocalRandomSeed变量。

ThreadLocalRandom原理的更多相关文章

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

    JUC源码分析-其它工具类(一)ThreadLocalRandom ThreadLocalRandom 是 JDK7 在 JUC 包下新增的随机数生成器,它解决了 Random 在多线程下多个线程竞争 ...

  2. JUC包中的分而治之策略-为提高性能而生

    一.前言 本次分享我们来共同探讨JUC包中一些有意思的类,包含AtomicLong & LongAdder,ThreadLocalRandom原理. 二.AtomicLong & Lo ...

  3. ThreadLocalRandom类原理分析

    1.Random类及其局限性 public int nextInt(int bound) { if (bound <= 0) throw new IllegalArgumentException ...

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

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

  5. Java并发编程原理与实战四十:JDK8新增LongAdder详解

    传统的原子锁AtomicLong/AtomicInt虽然也可以处理大量并发情况下的计数器,但是由于使用了自旋等待,当存在大量竞争时,会存在大量自旋等待,而导致CPU浪费,而有效计算很少,降低了计算效率 ...

  6. Spring cloud Hystrix使用@HystrixCommand使用Hystrix组件及@EnableCircuitBreaker原理介绍

    通过@HystrixCommand注解实现在Spring Cloud使用Hystrix组件相关的工程 cloud-registration-center:注册中心 cloud-service-hyst ...

  7. ConcurrentHashMap原理分析(1.7与1.8)-put和 get 需要执行两次Hash

    ConcurrentHashMap 与HashMap和Hashtable 最大的不同在于:put和 get 两次Hash到达指定的HashEntry,第一次hash到达Segment,第二次到达Seg ...

  8. (Concurrent)HashMap的存储过程及原理。

    1.前言 看完咕泡Jack前辈的有关hashMap的视频(非宣传,jack自带1.5倍嘴速,高效),收益良多,所以记录一下学习到的东西. 2.基础用法 源码的注释首先就介绍了哈希表是基于Map接口,所 ...

  9. JUC中的原子操作类及其原理

    昨天简单的看了看Unsafe的使用,今天我们看看JUC中的原子类是怎么使用Unsafe的,以及分析一下其中的原理! 一.简单使用AtomicLong 还记的上一篇博客中我们使用了volatile关键字 ...

随机推荐

  1. PAT L3-008. 喊山(BFS)C4 初赛30分

    喊山(30 分) 喊山,是人双手围在嘴边成喇叭状,对着远方高山发出“喂—喂喂—喂喂喂……”的呼唤.呼唤声通过空气的传递,回荡于深谷之间,传送到人们耳中,发出约定俗成的“讯号”,达到声讯传递交流的目的. ...

  2. Python Django框架 补充

    Django REST framework ORM框架整理 Django框架 app间互借models字段的操作 ORM数据库操作补充:models中的一对一操作.过滤.事务 Django model ...

  3. 第二章 深入分析Java I/O的工作机制(待续)

    Java的I/O类库的基本架构 磁盘I/O工作机制 网络I/O工作机制 NIO的工作方式 I/O调优 设计模式解析之适配器模式 设计模式解析之装饰器模式 适配器模式与装饰器模式的区别

  4. Laravel 在 with 查询中只查询个别字段

    在使用 Laravel 的关联查询中,我们经常使用 with 方法来避免 N+1 查询,但是 with 会将目标关联的所有字段全部查询出来,对于有强迫症的我们来说,当然是不允许的. 这时候我们可以使用 ...

  5. Android 4学习(2):概述 - AndroidManifes.xml

    Android应用程序包含下面这几个部分: 程序的前后台: Activities Services 存储: Content Providers 消息传递: Intents Broadcast Rece ...

  6. vb和dos批处理创建或生成快捷方式

    https://www.cnblogs.com/gszhl/archive/2009/04/23/1441753.html vb和dos批处理创建或生成快捷方式   首先说我现在用的一种,最有效的也是 ...

  7. Android添加Menu菜单

    在安卓中添加Menu菜单十分简单. 步骤: 1.在menu文件夹中的main.xml文件中添加要添加的项目. <menu xmlns:android="http://schemas.a ...

  8. Python之路:面向对象及相关

    其他相关 一.isinstance(obj, cls) 检查是否obj是否是类 cls 的对象 class Foo(object):     pass   obj = Foo()   isinstan ...

  9. dos 下bat 常用符号

    1.@一般在它之后紧跟一条命令或一条语句,则此命令或语句本身在执行的时候不会显示在屏幕上.请把下面的代码保存为test.cmd文件,然后运行,比较一下两条echo语句在屏幕上的输出差异:    ech ...

  10. 从公交塞车,看C#多线程问题(转)

    好久没写博客了,可能是因为最近工作太过于压抑的原因吧!有点颓废了.... 而且公司距离住处要坐公交将近40--50分钟(各个原因,纠结中ing...),提前一个半小时起床,居然还能迟到!因为距离公司前 ...