Java JUCThreadLocalRandom 类解析

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: ThreadLocalRandom 类解析

ThreadLocalRandom 类解析


前言

ThreadLocalRandom 类是 JDK7 在JUC包下新增的随机数生成器,它主要解决了 Random 类在多线程下的不足。


本文主要讲解为什么需要 ThreadLocalRandom 类,以及该类的实现原理。


Random 类及其局限性

首先我们先了解一下 Random 类。在 JDK7 以前到现在,java.util.Random 类都是使用较为广泛的随机数生成工具类,而且 java.lang.Math 的随机数生成也是使用的 java.util.Random 类的实例,下面先看看如何使用 Random 类。

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.print(random.nextInt(5)); //3421123432
        }
}

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


种子数只是随机算法的起始数字,和生成的随机数字的区间无关。
public Random() {
        this(seedUniquifier() ^ System.nanoTime());
}
public Random(long seed) {
        if (getClass() == Random.class)
            this.seed = new AtomicLong(initialScramble(seed));
        else {
            // subclass might have overriden setSeed
            this.seed = new AtomicLong();
            setSeed(seed);
        }
}

在有了默认种子之后,Random 是如何生成随机数的呢?我们看一下 nextInt()方法。

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

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


  • 根据老的种子生成新的种子
  • 然后根据新的种子来计算新的随机数


其中步骤4可以抽象为seed = f(seed),比如seed = f(seed) = a*seed+b

步骤5可以抽象为g(seed,bound),比如g(seed,bound) = (int) ((bound * (long) seed) >> 31)


在单线程情况下每次调用nextInt()都是根据老的种子计算出新的种子,这是可以保证随机数产生的随机性。但在多线程下多个线程可能拿同一个老的种子去执行步骤4以计算新的种子,这会导致多个线程的新种子是一样的,并且由于步骤5的算法是固定的,所以会导致多个线程产生相同的随机值


所以步骤4要保证原子性,也就是说当多个线程根据同一个老种子计算新种子时,第一个线程的新种子被计算出来后,第二个线程要丢弃自己老的种子,而使用第一个线程的新种子来计算自己的新种子,依此类推。


💡 在 Random 中使用了 原子变量 AtomicLong来达到这个效果, 在创建 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. 使用CAS操作,它使用新的种子去更新老的种子,失败的线程会通过循环重新获取更新后的种子作为当前种子去计算老的种子
        } while (!seed.compareAndSet(oldseed, nextseed));
        return (int)(nextseed >>> (48 - bits));
}

总结:在多线程下虽然同一时刻只有一个线程会成功,但是会造成大量线程进行自旋操作,这样会降低并发性能,所以 ThreadLocalRandom 运用而生。


ThreadLocalRandom

为了弥补 Random 在多线程下的效率问题,在 JUC 包中增加了 ThreadLocalRandom 类,下面先演示如何使用 ThreadLocalRandom 类:

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

其中第一步调用ThreadLocalRandom.current()来获取当前线程的随机数。


实际上 ThreadLocalRandom 的实现和 ThreadLocal 差不多,都是通过让每一个线程复制一份变量,从而让每个线程实际操作的都是本地内存中的副本,避免了对共享变量的同步。


Random 的缺点是多个线程会使用同一个原子性种子变量,从而导致对原子变量更新的竞争。

而在 ThreadLocalRandom 中,每个线程都维护一个种子变量,在每个线程生成随机数的时候都根据当前线程中旧的种子去计算新的种子,并使用新的种子更新老的种子,再根据新的种子去计算随机数,这样就不会存在竞争问题了。

image.png

源码解析

首先我们先看一下 ThreadLocalRandom 的类图结构。

image.png

从图中可以看到 ThreadLocalRandom 继承了 Random 类并重写了nextInt()方法,在 ThreadLocalRandom 类中并没有使用 Random 的原子性种子变量。


在 ThreadLocalRandom 中并没有存放具体的种子,具体的种子都放在具体的调用线程的 ThreadLocalRandomSeed 中变量中。


ThreadLocalRandom 类似于 ThreadLocal 类,是个工具类。当线程调用 ThreadLocalRandom 的 current()方法时,ThreadLocalRandom 负责初始化调用线程的 threadLocalRandomSeed 变量,进行初始化种子。


在调用 ThreadLocalRandom 的nextInt()方法时,实际就是获取当前线程的


ThreadLocalRandomSeed 变量作为当前种子去计算新种子,然后更新新的种子到 ThreadLocalRandomSeed 中,随后再根据新的种子去计算随机数。

需要注意的是:threadLocalRandomSeed 变量就是 Thread 类中的一个普通 long 类型的变量,不是原子类型变量。

@sun.misc.Contended("tlr")
long threadLocalRandomSeed;

因为这个变量是线程级别的,根本不需要使用原子类型变量。

ThreadLocalRandom 中的 seeder 和 probeGenerator 是两个原子性变量,将 probeGenerator 和 seeder 声明为原子变量的目的是为了在多线程情况下,赋予它们各自不同的种子初始值,这样就不会导致每个线程产生的随机数序列都是一样的,而且 probeGenerator 和 seeder 只会在初始化在初始化调用线程的种子和探针变量(用于分散计算数组索引下标)时候用到,每个线程只会使用一次。


另外变量 instance 是 ThreadLocalRandom 的一个实例,该变量是 static,多个线程使用的实例是同一个,但是由于具体的种子存在在线程里面的,所以在 ThreadlocalRandom 的实例里面只包含线程无关的的通用算法,因此它是线程安全的

下面来看一下 ThreadLocalRandom 类的主要代码逻辑:


1.Unsafe 机制

// Unsafe mechanics
    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 = 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实例里面的偏移量
            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() {
        if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
            localInit();
        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);
}

在上面代码中,如果当前线程中的 threadLocalRandomProbe 的变量值为 0(默认情况下线程的这个变量为 0),则说明当前线程是第一次调用current()方法,那么需要调用localInit()方法计算当前线程的初始化种子变量。


这里为了延迟初始化,在不需要随机数功能的时候就不初始化 Thread 类中的种子变量。


localInit()中,首先根据 probeGenerator 计算当前线程中的 threadLocalRandomProbe 初始化值,然后根据 seeder 计算当前线程的初始化种子,然后把这两个变量设置到当前线程中。


在 current 最后返回 ThreadLocalRandom 的实例。需要注意的是,这个方法是静态方法,多个线程返回的是同一个 ThreadLocalRandom 实例


3.int nextInt(int bound)方法

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

public int nextInt(int bound) {
        //参数校验
        if (bound <= 0)
            throw new IllegalArgumentException(BadBound);
        //根据当前线程中的种子计算新种子
        int r = mix32(nextSeed());
        //根据新种子计算随机数
        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; // read and update per-thread seed
        UNSAFE.putLong(t = Thread.currentThread(), SEED,
                       r = UNSAFE.getLong(t, SEED) + GAMMA);
        return r;
}

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


4.long initialSeed()方法

private static final AtomicLong seeder = new AtomicLong(initialSeed());
    private static long initialSeed() {
        String sec = VM.getSavedProperty("java.util.secureRandomSeed");
        if (Boolean.parseBoolean(sec)) {
            byte[] seedBytes = java.security.SecureRandom.getSeed(8);
            long s = (long)(seedBytes[0]) & 0xffL;
            for (int i = 1; i < 8; ++i)
                s = (s << 8) | ((long)(seedBytes[i]) & 0xffL);
            return s;
        }
        return (mix64(System.currentTimeMillis()) ^
                mix64(System.nanoTime()));
}

在初始化种子变量的初始值对应的原子变量 seeder 时,调用了initialSeed()方法,首先判断java.util.secureRandomSeed的系统属性值是否为 true 来判断是否使用安全性高的种子,如果为 true 则使用java.security.SecureRandom.getSeed(8)获取高安全性种子,如果为 false 则根据当前时间戳来获取初始化种子,也就是说使用安全性高的种子是无法被预测的,而 Random、ThreadLocalRandom 产生的被称为“伪随机数”,因为是可被预测的。


总结

ThreadLocalRandom 使用 ThreadLocal 的原理,让每个线程都持有一个本地的种子变量,该种子变量只有在使用随机数时才会被初始化。在多线程下计算新种子时是根据自己线程内维护的种子变量进行更新,从而避免了竞争。

相关文章
|
21天前
|
数据可视化 数据挖掘 BI
团队管理者必读:高效看板类协同软件的功能解析
在现代职场中,团队协作的效率直接影响项目成败。看板类协同软件通过可视化界面,帮助团队清晰规划任务、追踪进度,提高协作效率。本文介绍看板类软件的优势,并推荐五款优质工具:板栗看板、Trello、Monday.com、ClickUp 和 Asana,助力团队实现高效管理。
45 2
|
7天前
|
人工智能 自然语言处理 Java
FastExcel:开源的 JAVA 解析 Excel 工具,集成 AI 通过自然语言处理 Excel 文件,完全兼容 EasyExcel
FastExcel 是一款基于 Java 的高性能 Excel 处理工具,专注于优化大规模数据处理,提供简洁易用的 API 和流式操作能力,支持从 EasyExcel 无缝迁移。
55 9
FastExcel:开源的 JAVA 解析 Excel 工具,集成 AI 通过自然语言处理 Excel 文件,完全兼容 EasyExcel
|
14天前
|
存储 缓存 Java
Java 并发编程——volatile 关键字解析
本文介绍了Java线程中的`volatile`关键字及其与`synchronized`锁的区别。`volatile`保证了变量的可见性和一定的有序性,但不能保证原子性。它通过内存屏障实现,避免指令重排序,确保线程间数据一致。相比`synchronized`,`volatile`性能更优,适用于简单状态标记和某些特定场景,如单例模式中的双重检查锁定。文中还解释了Java内存模型的基本概念,包括主内存、工作内存及并发编程中的原子性、可见性和有序性。
Java 并发编程——volatile 关键字解析
|
14天前
|
JSON Java Apache
Java基础-常用API-Object类
继承是面向对象编程的重要特性,允许从已有类派生新类。Java采用单继承机制,默认所有类继承自Object类。Object类提供了多个常用方法,如`clone()`用于复制对象,`equals()`判断对象是否相等,`hashCode()`计算哈希码,`toString()`返回对象的字符串表示,`wait()`、`notify()`和`notifyAll()`用于线程同步,`finalize()`在对象被垃圾回收时调用。掌握这些方法有助于更好地理解和使用Java中的对象行为。
|
12天前
|
Java 数据库连接 Spring
反射-----浅解析(Java)
在java中,我们可以通过反射机制,知道任何一个类的成员变量(成员属性)和成员方法,也可以堆任何一个对象,调用这个对象的任何属性和方法,更进一步我们还可以修改部分信息和。
|
1月前
|
存储 算法 Java
Java内存管理深度解析####
本文深入探讨了Java虚拟机(JVM)中的内存分配与垃圾回收机制,揭示了其高效管理内存的奥秘。文章首先概述了JVM内存模型,随后详细阐述了堆、栈、方法区等关键区域的作用及管理策略。在垃圾回收部分,重点介绍了标记-清除、复制算法、标记-整理等多种回收算法的工作原理及其适用场景,并通过实际案例分析了不同GC策略对应用性能的影响。对于开发者而言,理解这些原理有助于编写出更加高效、稳定的Java应用程序。 ####
|
2月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
88 2
|
3月前
|
缓存 Java 程序员
Map - LinkedHashSet&Map源码解析
Map - LinkedHashSet&Map源码解析
89 0
|
13天前
|
存储 设计模式 算法
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。 行为型模式分为: • 模板方法模式 • 策略模式 • 命令模式 • 职责链模式 • 状态模式 • 观察者模式 • 中介者模式 • 迭代器模式 • 访问者模式 • 备忘录模式 • 解释器模式
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
|
13天前
|
设计模式 存储 安全
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。 结构型模式分为以下 7 种: • 代理模式 • 适配器模式 • 装饰者模式 • 桥接模式 • 外观模式 • 组合模式 • 享元模式
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析

热门文章

最新文章