ThraedLocalRandom类是JDK7在JUC包下新增的随机数生成器,它弥补了Random类在多线程下的缺陷。

Random类及其缺陷

下面看一下java.util.Random的使用方法。

import java.util.Random;

public class RandomTest1 {
public static void main(String[] args) {
//创建一个默认种子的随机数生成器
Random random = new Random();
//输出10个[0,5)范围的数
for (int i = 0; i < 10; i++) {
System.out.print(random.nextInt(5)+" ");
}
}
}
4 4 4 4 3 0 3 3 0 0
Process finished with exit code 0

默认种子的随机生成器使用的是默认的种子,这个种子是long类型的数字。

  public Random() {
this(seedUniquifier() ^ System.nanoTime());
}

有了默认种子后,如何生成随机数呢?我们查看一下nextInt()源码:

public int nextInt(int bound) {
//首先进行参数检查,判断输入的范围是否小于等于0
if (bound <= 0)
//如果小于等于0,则抛出非法参数异常。
throw new IllegalArgumentException(BadBound);
//根据老的种子生成新的种子,
int r = next(31);
//根据新的种子计算随机数。
int m = bound - 1;
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;
}

根据老的种子生成新的种子,我们可以想象成这样一个函数seed=f(seed),比如seed=f(seed)=a*seed+b;

根据新的种子计算生成数我们可以想像成g(seed,bound)=(int)(bound*(long)seed>>31)。在单线程下每次调用nextInt()方法都是根据老的的种子计算出新的种子,,这样可以保证随机数的产生是随机性的。但是在多线程下多个线程可能都会拿到同一个老的种子去执行根据老的种子生成新的种子以计算新的种子。这会导致多个线程产生的额新种子是一样的。由于根据新的种子计算随机数这个算法是不变的,所以在多线程下会产生相同的随机数。这并不是我们想要的。为了保证在多线程下每一个线程获取到的随机数不一样,当第一个线程的新种子计算出来之后,第二个线程就要丢弃掉自己的老种子,而是用第一个线程的新种子重新计算自己的新种子,以此类推,这样才能保证多线程下产生的随机数是随机的。Random函数使用了一个原子变量到达了这个效果,在创建Random对象时初始化的种子就被保存到种子原子变量里面,下面是next()方法源码:

protected int next(int bits) {
long oldseed, nextseed;
AtomicLong seed = this.seed;
do {
//1
oldseed = seed.get();
//2
nextseed = (oldseed * multiplier + addend) & mask;
//3
} while (!seed.compareAndSet(oldseed, nextseed));
//4
return (int)(nextseed >>> (48 - bits));
}

代码(1):获取当前原子变量种子的值。

代码(2):根据当前种子值计算新的种子。

代码(3):使用CAS操作,它使用新的种子来更新旧的种子,CAS操作会保证只有一个线程可以更新老的种子的新的,失败的线程会通过循环重新获取更新后的种子作为当前种子去计算老的种子,这就保证了随机数的随机性。

代码(4):适用固定算法根据新的种子计算随机数。

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

ThreadLocalRandom类

为了弥补高并发情况下Random的缺陷,在JUC包下新增了ThreadLocalRandom类,下面看一下如何使用它:

import java.util.concurrent.ThreadLocalRandom;

public class ThreadLocalRandomTest1 {
public static void main(String[] args) {
//(1)获取一个随机数生成器
ThreadLocalRandom random=ThreadLocalRandom.current();
//(2)输出10个[0,5)范围的数
for (int i = 0; i < 5; i++) {
System.out.print(random.nextInt(5)+" ");
}
}
}

运行结果

3 4 3 4 4
Process finished with exit code 0

代码(10)调用ThreadLocalRandom.current()方法来获取当前线程的随机数生成器,下面来分析一下ThreadLocalRandom的实现原理:ThreadLocal通过让每一个线程复制一份变量,使得在每个线程对变量进行线程操作时实际就是自己本地内存里面的副本,从而避免了对共享变量进行同步。实际上ThreadLocalRandom实现的也是这个原理,Random的缺点就是多个线程会使用同一个原子性种子变量,从而导致对原理变量更新的竞争,如图:

那么如果每一个线程都维护一个种子变量,则每个线程生成随机数都根据自己老的种子计算新的种子,并使用新的种子来更新老的种子,再根据新种子计算新的随机数。就不会存在竞争问题了,这会大大提高并发性。

源码分析

首先查看ThreadLocalRandom类结构:

从图中可以看出ThreadLocalRandom类继承了Random类,并重写了nextInt()方法,在ThreadLocalRandom类中并没有使用继承自Random类的原子性种子变量,在ThreadLocalRandom中并没有存放具体的种子,具体的种子存放在具体的调用线程的ThreadLocalRandom实例里面,ThreadLocalRandom类似于ThreadLocal类,是一个工具类,当线程调用ThreadLocalRandom.current()方法的时候,ThreadLocalRandom负责初始化调用ThreadLocalRandomSeed变量,也就是初始化种子。

当调用 ThreadLocalrandon的 nextInt方法时,实际上是获取当前线程的threadLocalRandom Seed变量作为当前种子来计算新的种子,然后更新新的种子到当前线程的 threadLocalRandom Seed变量,而后再根据新种子并使用具体算法计算随机数。这里需要注意的是, threadLocalRandom Seed变量就是 Thread类里面的一个普通long变量,它并不是原子性变量。

其中seeder和probeGenerator是两个原子性变量,在初始化调用线程的种子和探针变量时会引用它们,每个线程只会使用一次。

另外,变量 instance是 ThreadLocalRandom的一个实例,该变量是 static的。当多线程通过 ThreadLocalRandom的 current方法获取 ThreadLocalrandom的实例时,其实获取的是同一个实例。但是由于具体的种子是存放在线程里面的,所以在 Threadlocalrandom的实例里面只包含与线程无关的通用算法,所以它是线程安全的。

下面看看 ThreadLocalrandom的主要代码的实现逻辑。

  1. Unsafe机制

    private static final sun, misc. UnsafeUNSAFE
    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类里面 threadloca1 RandomSeed变量在 Thread实例里面的偏移量
    SEED= UNSAFE. objectFieldoffset
    (tk getDeclaredField( threadlocalRandomSeed ));
    //获取 Thread类里面 threadlocalrandomProbe变量在 Thread实例里面的偏移量
    PROBE= UNSAFE. objectFieldoffset
    (tk getDeclaredField("threadLocalRandomProbe" ));
    //获取 Thread类里面 threadLocalRandomSecondarySeed变量在 Thread实例里面的偏移
    量,这个值在后面讲解 LongAdder时会用到
    SECONDARY UNSAFE. objectFieldoffset
    (tk getDeclaredField(" threadLocalRandomSecondarySeed"));
    }catch (Exception e){
    throw new Error(e);
    }
    }
  2. ThreadLocalRandom current()方法。

    该方法获取ThreadLocalrandom实例对象,并初始化调用线程中的threadLocalRandomSeed和threadLocalRandomProbe变量。

    static final ThreadLocalRandom instance new ThreadLocalrandom(
    public static ThreadlocalRandom current (){
    //(1)
    if (UNSAFE getInt(Thread currentThread(), PROBE)==0)
    //(2)
    localInit();
    //(3)
    return instance;
    } static final void localInit{
    int p= probe Generator. addAndGet( PROBE INCremENT );
    int probe =(p==0)? 1: p;//skip 0
    long seed =mix64(seeder. getAndAdd (SEEDER INCREMENT));
    Thread t Thread currentThread();
    UNSAFE pulOng(t, SEED, seed);
    UNSAFE. putInt(t, PROBE, probe);
    }

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

    代码(2):首先根据 probeGenerator计算当前线程中 threadLocalRandom Probe的初始化值,然后根据 seeder计算当前线程的初始化种子,而后把这两个变量设置到当前线程。

    代码(3):返回 ThreadLocalRandom的实例。需要注意的是,这个方法是静态方法,多个线程返回的是同一个 ThreadLocalRandom实例。

  3. int nextInt(int bound)方法。

    计算当前线程的下一个随机数。

       public int nextInt(int bound) {
    //参数校验
    if (bound <= 0)
    throw new IllegalArgumentException(BadBound);
    //根据当前线程中的种子计算新种子
    int r = mix32(nextSeed());
    //根据新种子和bound计算随机数
    int m = bound - 1;
    if ((bound & m) == 0) // power of two
    r &= m;
    else { // reject over-represented candidates
    for (int u = r >>> 1;
    u + m - (r = u % bound) < 0;
    u = mix32(nextSeed()) >>> 1);
    }
    return r;
    }
  4. nextSeed方法。

    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;
    }

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

总结

该部分主要讲解了 Random的实现原理以及 Random在多线程下需要竞争种子原子变量

更新操作的缺点,从而引出 ThreadLocalRandom类。 Threadlocalrandom使用 Threadlocal

的原理,让每个线程都持有一个本地的种子变量,该种子变量只有在使用随机数时才会被

初始化。在多线程下计算新种子时是根据自己线程内维护的种子变量进行更新,从而避免

了竞争。

15-ThreadLocalRandom类剖析的更多相关文章

  1. Math&Random&ThreadLocalRandom类

    Math类 //绝对值值运算: Math.abs(18.999); //返回19.999这个数的绝对值 Math.abs(-12.58); // 返回-12.58这个数的绝对值,为12.58 //取值 ...

  2. Random类和ThreadLocalRandom类

    Random类和ThreadLocalRandom类 Random类用于生成一个伪随机数,他有两个构造方法:一个构造方法使用默认的种子(以当前时间作为种子),另一个构造方法需要显示传入一个long型整 ...

  3. Effective Java 第三版——15. 使类和成员的可访问性最小化

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  4. java中ThreadLocalRandom类和Random类的使用

    package frank; import java.lang.*; import java.util.*;//工具类一般都在util里面 import java.util.concurrent.Th ...

  5. Random类、ThreadLocalRandom类

    Random和ThreadLocalRandom类均用于生成伪随机数. Random的构造函数: Random()     默认以系统当前时间为种子,相当于Random(System.currentT ...

  6. Java基础15:深入剖析Java枚举类

    更多内容请关注微信公众号[Java技术江湖] 这是一位阿里 Java 工程师的技术小站,作者黄小斜,专注 Java 相关技术:SSM.SpringBoot.MySQL.分布式.中间件.集群.Linux ...

  7. learning java Random 和 ThreadLocalRandom类

    var rand = new Random(); System.out.println(rand.nextBoolean()); System.out.println(rand.nextInt()); ...

  8. ThreadLocalRandom类原理分析

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

  9. 面向对象15.3String类-常见功能-获取-1

    API使用: 查API文档的时候,有很多方法,首先先看返回的类型 下面的方法函数有的是有覆写Object类的如1.1图,如果没有复写的话是写在1.2图片那里的,如果找到了相对于的方法,可以点击进去可以 ...

随机推荐

  1. 聊聊spring事务失效的12种场景,太坑了

    前言 对于从事java开发工作的同学来说,spring的事务肯定再熟悉不过了. 在某些业务场景下,如果一个请求中,需要同时写入多张表的数据.为了保证操作的原子性(要么同时成功,要么同时失败),避免数据 ...

  2. You have mail in /var/mail/xxx

    因为配置 DDNS, 我添加了个 crontab 定时任务,每隔 1 分钟执行一段 python 脚本 然后就发现 terminal 经常提示 'You have mail in /var/mail/ ...

  3. 最详尽的 JS 原型与原型链终极详解(1)(2)(3)===转载

    转载===方便以后复习 原文网址:https://www.jianshu.com/p/dee9f8b14771 一. 普通对象与函数对象 JavaScript 中,万物皆对象!但对象也是有区别的.分为 ...

  4. Mysql - You can't specify target table '表名' for update in FROM clause 错误解决办法

    背景 在MySQL中,写SQL语句的时候 ,可能会遇到 You can't specify target table '表名' for update in FROM clause 这样的错误 错误含义 ...

  5. 算法:实现strStr(),字符串indexOf方法

    描述 给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始).如果不存在,则返回  -1. 个人思路: ...

  6. openswan IPSec专栏目录锦集

    为了方便查阅现有的文章,特准备一个目录页供后续查询使用 专栏序言 1. 基础知识 openswan任务调度基础知识之信号 2. openswan环境搭建 openswan框架和编译时说明 opensw ...

  7. 用Java写了一个程序,将一个Mysql库中的表,迁移到另外一个server上的Mysql库中

    用Navicat做数据迁移,因为数据量比较大,迁移过过程中一个是进展不直观,另外就是cpu占用率高的时候,屏幕跟死机了一样点不动按钮,不好中断. 想了想,干脆自己写一个. 在网上找了一个sqllite ...

  8. 关于pycharm创建django1.x和3.x项目的说明

    1.我创建了两个模板文件分别代表django1.x和3.x 2.两个模板文件分别为Django1Template和Django3Template (不同模板文件中存放不同的django版本) 3.使用 ...

  9. 基于swoole框架hyperf开发的纯API接口化的后台RBAC管理工具hyperfly@v1.0.0发布

    hyperfly@v1.0.0发布 本文地址http://yangjianyong.cn/?p=323转载无需经过作者本人授权 github地址:https://github.com/vankour/ ...

  10. 修改 CubeMX 生成的 RT-Thread makefile 工程

    修改 CubeMX 生成的 RT-Thread makefile 工程 使用 RT-Thread 官方 基于 CubeMX 移植 RT-Thread Nano 生成的 Makefile 工程在编译时有 ...