Java JUCThreadLocalRandom 类解析

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 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 的原理,让每个线程都持有一个本地的种子变量,该种子变量只有在使用随机数时才会被初始化。在多线程下计算新种子时是根据自己线程内维护的种子变量进行更新,从而避免了竞争。

相关文章
|
1天前
|
算法 Java 数据处理
从HashSet到TreeSet,Java集合框架中的Set接口及其实现类以其“不重复性”要求,彻底改变了处理唯一性数据的方式。
从HashSet到TreeSet,Java集合框架中的Set接口及其实现类以其“不重复性”要求,彻底改变了处理唯一性数据的方式。HashSet基于哈希表实现,提供高效的元素操作;TreeSet则通过红黑树实现元素的自然排序,适合需要有序访问的场景。本文通过示例代码详细介绍了两者的特性和应用场景。
14 6
|
1天前
|
存储 Java
深入探讨了Java集合框架中的HashSet和TreeSet,解析了两者在元素存储上的无序与有序特性。
【10月更文挑战第16天】本文深入探讨了Java集合框架中的HashSet和TreeSet,解析了两者在元素存储上的无序与有序特性。HashSet基于哈希表实现,添加元素时根据哈希值分布,遍历时顺序不可预测;而TreeSet利用红黑树结构,按自然顺序或自定义顺序存储元素,确保遍历时有序输出。文章还提供了示例代码,帮助读者更好地理解这两种集合类型的使用场景和内部机制。
9 3
|
1天前
|
存储 算法 Java
Java Set深度解析:为何它能成为“无重复”的代名词?
Java Set深度解析:为何它能成为“无重复”的代名词?本文详解Set接口及其主要实现类(HashSet、TreeSet、LinkedHashSet)的“无重复”特性,探讨其内部数据结构和算法实现,并通过示例代码展示最佳实践。
7 3
|
3天前
|
IDE Java 编译器
Java:如何确定编译和运行时类路径是否一致
类路径(Classpath)是JVM用于查找类文件的路径列表,对编译和运行Java程序至关重要。编译时通过`javac -classpath`指定,运行时通过`java -classpath`指定。IDE如Eclipse和IntelliJ IDEA也提供界面管理类路径。确保编译和运行时类路径一致,特别是外部库和项目内部类的路径设置。
|
2天前
|
安全 Java 测试技术
Java零基础-StringBuffer 类详解
【10月更文挑战第9天】Java零基础教学篇,手把手实践教学!
10 2
|
3天前
|
算法 Java 数据处理
从HashSet到TreeSet,Java集合框架中的Set接口及其实现类以其独特的“不重复性”要求,彻底改变了处理唯一性约束数据的方式。
【10月更文挑战第14天】从HashSet到TreeSet,Java集合框架中的Set接口及其实现类以其独特的“不重复性”要求,彻底改变了处理唯一性约束数据的方式。本文深入探讨Set的核心理念,并通过示例代码展示了HashSet和TreeSet的特点和应用场景。
9 2
|
3天前
|
存储 Java 索引
Java 中集合框架的常见接口和类
【10月更文挑战第13天】这些只是集合框架中的一部分常见接口和类,还有其他一些如 Queue、Deque 等接口以及相关的实现类。理解和掌握这些集合的特点和用法对于高效编程非常重要。
|
4天前
|
存储 监控 算法
Java中的内存管理与垃圾回收机制解析
本文深入探讨了Java编程语言中的内存管理方式,特别是垃圾回收机制。我们将了解Java的自动内存管理是如何工作的,它如何帮助开发者避免常见的内存泄漏问题。通过分析不同垃圾回收算法(如标记-清除、复制和标记-整理)以及JVM如何选择合适的垃圾回收策略,本文旨在帮助Java开发者更好地理解和优化应用程序的性能。
|
10天前
|
缓存 Java 程序员
Map - LinkedHashSet&Map源码解析
Map - LinkedHashSet&Map源码解析
26 0
|
10天前
|
算法 Java 容器
Map - HashSet & HashMap 源码解析
Map - HashSet & HashMap 源码解析
25 0

推荐镜像

更多