首页 > 其他分享 >【JDK】Random 的局限以及ThreadLocalRandom 类原理剖析

【JDK】Random 的局限以及ThreadLocalRandom 类原理剖析

时间:2024-02-09 11:44:06浏览次数:32  
标签:ThreadLocalRandom JDK Random 线程 种子 随机数 变量

1  前言

我们平时使用随机数大家可能会用到 Random,但是它的问题大家知道吗?以及该如何解决呢?这节我们就来看看。

2  Random 类及其局限性

在 JDK 7 之前包括现在,java.util.Random 都是使用比较广泛的随机数生成工具类, 而且 java.lang.Math 中的随机数生成也使用的是 java.util.Random 的实例。下面先看看 java. util.Random 的使用方法。

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

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

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

随机数的生成需要一个默认的种子,这个种子其实是一个 long 类型的数字 , 你可以在创建 Random 对象时通过构造函数指定,如果不指定则在默认构造函数内部生成一个默认 的值。有了默认的种子后,如何生成随机数呢?

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

由此可见,新的随机数的生成需要两个步骤 :

(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 >>> (48 - bits));
}

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

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

代码(8)使用 CAS 操作,它使用新的种子去更新老的种子,在多线程下可能多个线 程都同时执行到了代码(6),那么可能多个线程拿到的当前种子的值是同一个,然后执行 步骤(7)计算的新种子也都是一样的,但是步骤(8)的 CAS 操作会保证只有一个线程 可以更新老的种子为新的,失败的线程会通过循环重新获取更新后的种子作为当前种子去 计算老的种子,这就解决了上面提到的问题,保证了随机数的随机性。

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

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

首先 Random 在多线程下并不会存在数据错误或者结果错误,而是在多线程环境下它的性能会有所下降。

3  ThreadLocalRandom

ThreadLocalRandom 类是 JDK 7 在 JUC 包下新增的随机数生成器,为了弥补多线程高并发情况下 Random 的缺陷,在 JUC 包下新增了 ThreadLocalRandom 类。下面首先看下如何使用它。

public class RandomTest {
    public static void main(String[] args) {
        //(10)获取一个随机数生成器
        ThreadLocalRandom random = ThreadLocalRandom.current();

        //(11)输出10个在0~5(包含0,不包含5)之间的随机数
        for (int i = 0; i < 10; ++i) {
            System.out.println(random.nextInt(5));
        }
    }
}

其中,代码(10)调用 ThreadLocalRandom.current() 来获取当前线程的随机数生成器。 下面来分析下 ThreadLocalRandom 的实现原理。从名字上看它会让我们联想到在基础篇中 讲解的 ThreadLocal :ThreadLocal 通过让每一个线程复制一份变量,使得在每个线程对变 量进行操作时实际是操作自己本地内存里面的副本,从而避免了对共享变量进行同步。实 际上 ThreadLocalRandom 的实现也是这个原理,Random 的缺点是多个线程会使用同一个 原子性种子变量,从而导致对原子变量更新的竞争,如图 3-1 所示。

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

3.1  源码分析

首先看下 ThreadLocalRandom 的类图结构,如图 3-3 所示。

从图中可以看出 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 的主要代码的实现逻辑。

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

3.1.2  ThreadLocalRandom current() 方法

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

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

3.1.3  int nextInt(int bound) 方法

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

public int nextInt(int bound) {
    //(15)参数校验
    if (bound <= 0)
        throw new IllegalArgumentException(BadBound);
    //(16) 根据当前线程中的种子计算新种子
    int r = mix32(nextSeed());
    //(17)根据新种子和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;
}

如上代码的逻辑步骤与 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 变量中。

3.2  延申

平时有用过 hutool 的工具包的话,其中的随机工具类也是使用的 ThreadLocalRandom 实现的。

4  小结

好啦,大家知道了 Random 的实现原理以及 Random 在多线程下需要竞争种子原子变量 更新操作的缺点,从而引出 ThreadLocalRandom 类。ThreadLocalRandom 使用 ThreadLocal 的原理,让每个线程都持有一个本地的种子变量,该种子变量只有在使用随机数时才会被 初始化。在多线程下计算新种子时是根据自己线程内维护的种子变量进行更新,从而避免 了竞争。

标签:ThreadLocalRandom,JDK,Random,线程,种子,随机数,变量
From: https://www.cnblogs.com/kukuxjx/p/18012390

相关文章

  • Jenkins在jdk17的Tomcat上运行报错
    Jenkins在jdk17的Tomcat上运行报错一、环境宝塔:tomcat8.0jdk:jdk17二、保存项目时报错​Unabletomakefieldprotectedtransientintjava.util.AbstractList.modCountaccessible:modulejava.basedoesnot"opensjava.util"tounnamedmodule@6d15ca84​查看local......
  • Jenkins在jdk17的Tomcat上运行报错
    Jenkins在jdk17的Tomcat上运行报错一、环境宝塔:tomcat8.0jdk:jdk17二、保存项目时报错​Unabletomakefieldprotectedtransientintjava.util.AbstractList.modCountaccessible:modulejava.basedoesnot"opensjava.util"tounnamedmodule@6d15ca84​查看local......
  • Jenkins在jdk17的Tomcat上运行报错
    Jenkins在jdk17的Tomcat上运行报错一、环境宝塔:tomcat8.0jdk:jdk17二、保存项目时报错​Unabletomakefieldprotectedtransientintjava.util.AbstractList.modCountaccessible:modulejava.basedoesnot"opensjava.util"tounnamedmodule@6d15ca84​查看local......
  • 【JAVA】JDK8~17の新しい機能の紹介
    JDK8(2014年发布):Lambda表达式:允许在Java中使用函数式编程风格,简化代码并提高可读性。StreamAPI:提供了一种新的数据处理方式,支持函数式编程和并行处理。接口的默认方法和静态方法:允许在接口中定义具有实现的方法,提高了接口的灵活性。新的日期和时间API:引入了java.time包,提......
  • CentOS安装配置JDK详细教程
    CentOS安装配置JDK详细教程一、查询系统中自带的JDKrpm-qa|grepjdk二、卸载系统中自带的JDK#方式一逐个卸载rpm-e--nodepsjdk的rpm软件包名#方式二传参一键卸载rpm-qa|grepjdk|xargsrpm-e--nodeps三、将JDK的.tar.gz压缩包上传至/opt目录四、解压J......
  • CentOS安装配置JDK详细教程
    CentOS安装配置JDK详细教程一、查询系统中自带的JDKrpm-qa|grepjdk二、卸载系统中自带的JDK逐个卸载rpm-e--nodepsjdk的rpm软件包名全部卸载rpm-qa|grepjdk|xargsrpm-e--nodeps三、将JDK的.tar.gz压缩包上传至/opt目录四、解压JDK压缩包tar-z......
  • [Java]静态代理、动态代理(基于JDK1.8)
    【版权声明】未经博主同意,谢绝转载!(请尊重原创,博主保留追究权)https://www.cnblogs.com/cnb-yuchen/p/18002823出自【进步*于辰的博客】参考笔记一,P83;笔记二,P75.4。目录1、静态代理1.1概述1.2静态代理的两种形式1.2.1面向接口1.2.2面向继承2、动态代理2.1什么是动态代......
  • 【设计模式】原型模式——JDK源码中的原型模式
    原型模式在JDK源码中有广泛的应用,想找原型模式的实现也比较容易,只要找Cloneable接口的子类即可。因为JDK源码中Cloneable的子类太多,本文只讨论最典型的几种。ArrayList首先看ArrayList和原型模式有关的代码:publicclassArrayList<E>extendsAbstractList<E>implementsL......
  • 自定义jdk镜像
    1、安装docker这里使用Ubuntu系统安装docker,方便一点(偷懒)apt-getinstalldocker.io-y安装不了可以试试阿里云镜像源,在/etc/apt/sources.list.d/下新增文件:aliyun.sources.list,写入如下内容(ubuntu22.04示例,其他版本参考:ubuntu镜像_ubuntu下载地址_ubuntu安装教程-阿里巴巴......
  • [转帖]Open JDK 8.0_152-b16 崩溃 : [libzip.so+0x12522] newEntry+0x62
    一.问题描述在执行spark任务的时候,JVM崩溃.崩溃dump日志:##AfatalerrorhasbeendetectedbytheJavaRuntimeEnvironment:##SIGBUS(0x7)atpc=0x00007f9adacb9522,pid=107874,tid=0x00007f9add417700##JREversion:Java(TM)SERuntimeEnvironme......