Java Review - 并发编程_ThreadLocalRandom实现原理&源码分析

简介: Java Review - 并发编程_ThreadLocalRandom实现原理&源码分析

195d03d17afc4a928bc581f313b01dfe.png


概述


ThreadLocalRandom类是JDK 7在JUC包下新增的随机数生成器,它弥补了Random类在多线程下的缺陷。我们这里主要讲解为何要在JUC下新增该类,以及该类的实现原理。


Random的局限性


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

import java.util.Random;
/**
 * @author 小工匠
 * @version 1.0
 * @description: TODO
 * @date 2021/11/28 23:05
 * @mark: show me the code , change the world
 */
public class RandomTest {
    public static void main(String[] args) {
        // 1
        Random random = new Random();
        for (int i = 0; i < 10; i++) {
            // 2
            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 根据新的种子计算随机数
        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),其中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应运而生。


ThreadLocalRandom使用及原理


为了弥补多线程高并发情况下Random的缺陷,在JUC包下新增了ThreadLocalRandom类.


使用

import java.util.concurrent.ThreadLocalRandom;
/**
 * @author 小工匠
 * @version 1.0
 * @description: TODO
 * @date 2021/11/28 23:28
 * @mark: show me the code , change the world
 */
public class ThreadLocalRandomTest {
    public static void main(String[] args) {
        // 10 获取一个随机数生成器
        ThreadLocalRandom tr = ThreadLocalRandom.current();
        // 11 输出10个(包括0 不包括5)之间的随机数
        for (int i = 0; i < 10; i++) {
            System.out.println(tr.nextInt(5));
        }
    }
}


  • 代码(10)调用ThreadLocalRandom.current()来获取当前线程的随机数生成器


0b63346ea43d448b8281d3138817e3a5.png


原理


从名字上看它会让我们联想到ThreadLocal:ThreadLocal通过让每一个线程复制一份变量,使得在每个线程对变量进行操作时实际是操作自己本地内存里面的副本,从而避免了对共享变量进行同步。


实际上ThreadLocalRandom的实现也是这个原理,Random的缺点是多个线程会使用同一个原子性种子变量,从而导致对原子变量更新的竞争.


472a41f1b35f4bd88cf642bb05f483dc.png


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


4f0df6431f964d4ca652decb74a8b469.png


bdcbdc80a54f4009b965e139c6e720c2.png


ThreadLocalRandom源码分析


3819882637bf41af9aa1a1c3e5beb20c.png


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 current() 该方


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

    /**
     * Returns the current thread's {@code ThreadLocalRandom}.
     *
     * @return the current thread's {@code ThreadLocalRandom}
     */
    public static ThreadLocalRandom current() {
         // 12 
        if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
            // 13 
            localInit();
            // 14
        return instance;
    }
   /**
     * Initialize Thread fields for the current thread.  Called only
     * when Thread.threadLocalRandomProbe is zero, indicating that a
     * thread local seed value needs to be generated. Note that even
     * though the initialization is purely thread-local, we need to
     * rely on (static) atomic generators to initialize the values.
     */
    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实例。


int nextInt(int bound)

    public int nextInt(int bound) {
       // 15  
        if (bound <= 0)
            throw new IllegalArgumentException(BadBound);
        // 16 
        int r = mix32(nextSeed());
        // 17 
        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变量中。


小结


我们这里主要阐述了Random的实现原理以及Random在多线程下需要竞争种子原子变量更新操作的缺点,从而引出ThreadLocalRandom类。


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


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


d5333b7d0d3a42f588882d8c5eea1ff4.png


相关文章
|
10月前
|
存储 缓存 安全
Java HashMap详解及实现原理
Java HashMap是Java集合框架中常用的Map接口实现,基于哈希表结构,允许null键和值,提供高效的存取操作。它通过哈希函数将键映射到数组索引,并使用链表或红黑树解决哈希冲突。HashMap非线程安全,多线程环境下需注意并发问题,常用解决方案包括ConcurrentHashMap和Collections.synchronizedMap()。此外,合理设置初始化容量和加载因子、重写hashCode()和equals()方法有助于提高性能和避免哈希冲突。
594 17
Java HashMap详解及实现原理
|
监控 Java 开发者
深入理解Java中的线程池实现原理及其性能优化####
本文旨在揭示Java中线程池的核心工作机制,通过剖析其背后的设计思想与实现细节,为读者提供一份详尽的线程池性能优化指南。不同于传统的技术教程,本文将采用一种互动式探索的方式,带领大家从理论到实践,逐步揭开线程池高效管理线程资源的奥秘。无论你是Java并发编程的初学者,还是寻求性能调优技巧的资深开发者,都能在本文中找到有价值的内容。 ####
|
安全 Java 程序员
深入理解Java内存模型与并发编程####
本文旨在探讨Java内存模型(JMM)的复杂性及其对并发编程的影响,不同于传统的摘要形式,本文将以一个实际案例为引子,逐步揭示JMM的核心概念,包括原子性、可见性、有序性,以及这些特性在多线程环境下的具体表现。通过对比分析不同并发工具类的应用,如synchronized、volatile关键字、Lock接口及其实现等,本文将展示如何在实践中有效利用JMM来设计高效且安全的并发程序。最后,还将简要介绍Java 8及更高版本中引入的新特性,如StampedLock,以及它们如何进一步优化多线程编程模型。 ####
169 0
|
缓存 Java 开发者
Java多线程并发编程:同步机制与实践应用
本文深入探讨Java多线程中的同步机制,分析了多线程并发带来的数据不一致等问题,详细介绍了`synchronized`关键字、`ReentrantLock`显式锁及`ReentrantReadWriteLock`读写锁的应用,结合代码示例展示了如何有效解决竞态条件,提升程序性能与稳定性。
860 6
|
设计模式 安全 Java
Java 多线程并发编程
Java多线程并发编程是指在Java程序中使用多个线程同时执行,以提高程序的运行效率和响应速度。通过合理管理和调度线程,可以充分利用多核处理器资源,实现高效的任务处理。本内容将介绍Java多线程的基础概念、实现方式及常见问题解决方法。
401 1
|
存储 缓存 安全
Java内存模型(JMM):深入理解并发编程的基石####
【10月更文挑战第29天】 本文作为一篇技术性文章,旨在深入探讨Java内存模型(JMM)的核心概念、工作原理及其在并发编程中的应用。我们将从JMM的基本定义出发,逐步剖析其如何通过happens-before原则、volatile关键字、synchronized关键字等机制,解决多线程环境下的数据可见性、原子性和有序性问题。不同于常规摘要的简述方式,本摘要将直接概述文章的核心内容,为读者提供一个清晰的学习路径。 ####
206 2
|
Java
Java基础之 JDK8 HashMap 源码分析(中间写出与JDK7的区别)
这篇文章详细分析了Java中HashMap的源码,包括JDK8与JDK7的区别、构造函数、put和get方法的实现,以及位运算法的应用,并讨论了JDK8中的优化,如链表转红黑树的阈值和扩容机制。
225 1
|
XML 存储 Java
Java Review(三十三、异常处理----补充:断言、日志、调试)
Java Review(三十三、异常处理----补充:断言、日志、调试)
265 0
|
机器学习/深度学习 Java 程序员
Java Review(三十二、异常处理)
Java Review(三十二、异常处理)
197 0
Java Review(三十二、异常处理)
|
2月前
|
JSON 网络协议 安全
【Java】(10)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
199 1