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. centos6.10下安装mysql8.0.16root密码修改的坑

    上图截取别人的自己懒得弄,检查自己的linux是否有安装就按上图做就行了 接下来是我的干货 mysql8.0安群策略对密码设置很严格规则:大小写加数字和特殊字符串 使用yum安装mysql 后 my. ...

  2. go实现堆排序

    package main import "fmt" func main(){ arr:=[]int{4,8,2,1,6,9,3,5,7,8,1,4} dui(arr) fmt.Pr ...

  3. 🏆【JVM技术专区】「难点-核心-遗漏」TLAB内存分配+锁的碰撞(技术串烧)!

    JVM内存分配及申请过程 当使用new关键字或者其他任何方式进行创建一个类的对象时,JVM虚拟机需要为该对象分配内存空间,而对象的大小在类加载完成后已经确定了,所以分配内存只需要在Java堆中划分出一 ...

  4. 在CentOs7源码安装mysql-5.6.35单实例数据库

    首先安装依赖包,避免在安装过程中出现问题 [root@bogon liuzhen]# yum -y install gcc gcc-c++[root@bogon liuzhen]# yum -y in ...

  5. Session原理、生命周期及购物车功能的实现

    在WEB开发中,服务器可以为每个用户浏览器创建一个会话对象(session对象),注意:一个浏览器独占一个session对象(默认情况下).因此,在需要保存用户数据(保存该浏览器(会话)的相关信息)时 ...

  6. 【机器学习|数学基础】Mathematics for Machine Learning系列之线性代数(1):二阶与三阶行列式、全排列及其逆序数

    @ 目录 前言 二阶与三阶行列式 二阶行列式 三阶行列式 全排列及其逆序数 全排列 逆序数 结语 前言 Hello!小伙伴! 非常感谢您阅读海轰的文章,倘若文中有错误的地方,欢迎您指出-   自我介绍 ...

  7. 内核软中断之tasklet机制

    1. 软中断IRQ简介 软中断(SoftIRQ)是内核提供的一种基于中断的延时机制, Linux内核定义的软中断有以下几种: enum { HI_SOFTIRQ=0, /*高优先级的tasklet*/ ...

  8. 【第四篇】- Maven 构建生命周期之Spring Cloud直播商城 b2b2c电子商务技术总结

    ​ ​ Maven 构建生命周期 Maven 构建生命周期定义了一个项目构建跟发布的过程. 一个典型的 Maven 构建(build)生命周期是由以下几个阶段的序列组成的: ​ 阶段 处理 描述 验证 ...

  9. 什么是maven与maven的使用过程(例如在idea创建maven工程(重点讲讲idea创建使用maven管理的web工程,并且部署到tomcat上))

    什么是maven与maven的使用过程(例如在idea创建maven工程) (重点讲讲idea创建使用maven管理的web工程项目,并且部署到tomcat服务器上) 一.什么是maven? 1, M ...

  10. 洛谷P1019——单词接龙(DFS暴力搜索)

    https://www.luogu.org/problem/show?pid=1019#sub 题目描述 单词接龙是一个与我们经常玩的成语接龙相类似的游戏,现在我们已知一组单词,且给定一个开头的字母, ...