初探 JUC 并发编程:ThreadLocalRandom 原理剖析

2024-04-29 01:52

本文主要是介绍初探 JUC 并发编程:ThreadLocalRandom 原理剖析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

最近在阅读 Java 并发编程之美这本书,感觉学到了很多东西;所以我决定将从事书中学到的思想和一些经典的案例整理成博客的形式与大家分享和交流,如果对大家有帮助别忘了留下点赞和关注捏。

3.1)Random 类的局限性

在 JDK1.7 之前,java.util.Random 都是应用比较广泛的随机数生成工具,java.lang.Math 中的随机数生成也是使用的 Random 的实例。

写一段代码来测试一下 Random 实例:

public class RandomTest {public static void main(String[] args) {Random random = new Random();for (int i = 1; i < 10; i++) {// 输出 10 个在 0 到 5 之间的随机数,不包括 5System.out.println(random.nextInt(5));}}
}

上面展示了一个随机数生成器,生成 10 个 0 到 5 之间的随机数,下面来看一下具体的实现:

    public int nextInt(int bound) {if (bound <= 0) // 参数检验throw new IllegalArgumentException(BadBound);int r = next(31); // 生成一个 31 位的随机正数 rint m = bound - 1; if ((bound & m) == 0)  // 判断 bound 是不是 2 的幂次r = (int)((bound * (long)r) >> 31); // 生成随机数else {for (int u = r;u - (r = u % bound) + m < 0; // 不断对 bound 进行取模运算u = next(31)) // 生成随机数;}return r;}

上面代码的操作逻辑是这样的:

  1. nextInt(int bound) 方法接收一个整数参数 bound,表示生成的随机数的上限(不包括)。如果传入的 bound 小于等于0,则抛出 IllegalArgumentException 异常。
  2. int r = next(31); 生成一个31位的随机整数 r
  3. int m = bound - 1; 计算 bound 减去1的值。
  4. if ((bound & m) == 0) 判断 bound 是否是2的幂,即 boundm 的按位与操作结果是否为0。如果是,说明 bound 是2的幂,这时采用一种优化方法来生成随机数。
  5. 如果 bound 是2的幂,则 r 的计算方法为 (int)((bound * (long)r) >> 31)。这个式子的意思是将 bound 乘以 r,然后右移31位,最终将结果转换为整数,这样就生成了一个在0到 bound-1 之间的随机数 r
  6. 如果 bound 不是2的幂,则进入 else 分支。
  7. else 分支中,使用循环来生成一个满足条件的随机数 r。循环中不断地生成新的随机数 u,然后对 u 取模 bound,直到生成的 r 落在0到 bound-1 之间。
  8. 返回生成的随机数 r

通过上面的流程我们可以了解到,这个方法中生成随机数主要是调用了 next() 方法,下面看一下它的具体实现:

    protected int next(int bits) {long oldseed, nextseed;AtomicLong seed = this.seed;do {oldseed = seed.get();nextseed = (oldseed * multiplier + addend) & mask;} while (!seed.compareAndSet(oldseed, nextseed));return (int)(nextseed >>> (48 - bits));}

这个方法用于升成一个指定位数的随机数:

  1. 首先,定义了两个 long 类型的变量 oldseednextseed,用来存储随机数生成器的种子值。
  2. AtomicLong seed = this.seed; 将当前对象中的 seed 变量赋值给局部变量 seedseed 可能是一个原子长整型变量。
  3. do-while 循环用于生成随机数,直到成功更新种子值。循环的条件是 seed.compareAndSet(oldseed, nextseed),即当 seed 的值与 oldseed 相等时,将 nextseed 的值赋给 seed,如果更新成功,则跳出循环,否则继续生成下一个随机数。
  4. 在循环中,随机数的生成通过下面的计算完成:nextseed = (oldseed * multiplier + addend) & mask;。这里使用了线性同余算法来生成伪随机数,即将当前种子值乘以一个常数 multiplier,然后加上另一个常数 addend,最后对结果进行位与操作并截取低位以得到下一个种子值。
  5. 生成的随机数由 (int)(nextseed >>> (48 - bits)) 计算得到。这里首先将 nextseed 右移 48 - bits 位,然后将结果强制转换为整数,得到指定位数的随机数。

随机数的生成依赖于种子 seed,在县城城的条件下不会有什么问题,但是如果是在多线程的情况下,存在潜在的种子重复的问题,多个线程仍然可能会在同一时刻读取到相同的种子值


带着这个问题再来看 nextInt() 方法,当很多线程利用相同的种子进行更新的操作的时候,由于上面的操作是 CAS 操作,同时只有一个线程成功,会造成大量的线程重试,这就降低了并发的性能,所以 ThreadLocalRandom 应运而生。

3.2)ThreadLocalRandom

先写一段代码来展示如何使用它:

public class ThreadLocalRandomTest {public static void main(String[] args) {ThreadLocalRandom random = ThreadLocalRandom.current();for (int i = 0; i < 10; i++) {// 生成随机数System.out.println(random.nextInt(5));}}
}

看到这个名字,很容易就能联想到 ThreadLocal,ThreadLocal 的原理是让每个线程复制一份变量,每个线程操作自己的副本,从而避免多个线程之间的同步问题,ThreadLocalRandom 同样也是这个原理,Random 的缺点就是多个线程使用一个 seed,从而引发竞争的情况;但如果每个线程都维护一个种子变量就不会存在并发的问题了,从而大大的提高了并发的性能。
在这里插入图片描述
ThreadLocalRandom 的继承关系是这样的,它继承了 Random 类并且重写了 nextInt 方法,ThreadLocalRandom 类中使用的种子存放在调用线程的 threadLocalRandomSeed 变量中,当线程调用了 ThreadLocalRandom 类的 current() 方法的时候,ThreadLocalRandom 会去初始化调用线程的 threadLocalRandomSeed 变量,也就是初始化种子。

    public static ThreadLocalRandom current() {if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)localInit();return instance;}

上面的方法就是 current 方法,它先通过 Unsafe 类获取到了当前线程偏移量为 PROBE 的值,如果发现其值为 0(没有被初始化过),就会调用了 localInit 方法来初始化,最终会返回一个 instance,这是一个 ThreadLocalRandom 的实例:
static final ThreadLocalRandom *instance* = new ThreadLocalRandom();

可以保证多个线程访问 current() 来生成 Random 调用的是相同的 ThreadLocalRandom 实例,在 ThreadLocalRandom 实例中只包含和线程无关的通用算法,所以它是线程安全的。

为什么说发现 PROBE 是 0 就说明没有被初始化呢?
在 Thread 类中可以一探究竟,这个属性的注解是这样的:Probe hash value; nonzero if threadLocalRandomSeed initialized,这是一个散列探测值,如果这个 threadLocalRandomSeed 被初始化,则它是非零的;这个值没被赋任何初始值,所以在类被实例化创建的时候会被初始化为默认值,也就是 0。

下面来看一下 localInit 的具体实现:

    static final void localInit() {int p = probeGenerator.addAndGet(PROBE_INCREMENT);int probe = (p == 0) ? 1 : p; // skip 0long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));Thread t = Thread.currentThread();UNSAFE.putLong(t, SEED, seed);UNSAFE.putInt(t, PROBE, probe);}

方法中首先生成了 seed 和 probe(均为随机数生成过程中需要维护和更新的变量),然后将这两个变量分别赋值给对象中偏移量为 SEED 和 PROBE 的属性。

偏移量是 Java 中的 Unsafe 类用来直接操控内存中变量使用的一种标识,通过这个标示就能找到内存中对应的变量属性,这个偏移量通过 UNSAFE.objectFieldOffset(Field field) 来获得,下面代码中展示了获取这两个偏移量的方法:

    // Unsafe mechanicsprivate 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;SEED = UNSAFE.objectFieldOffset(tk.getDeclaredField("threadLocalRandomSeed"));PROBE = UNSAFE.objectFieldOffset(tk.getDeclaredField("threadLocalRandomProbe"));SECONDARY = UNSAFE.objectFieldOffset(tk.getDeclaredField("threadLocalRandomSecondarySeed"));} catch (Exception e) {throw new Error(e);}}

3.3)nextInt() 方法

通过上面的讲解,大家对 ThreadLocalRandom 有了基本的理解,下面来看一下在 nextInt() 方法中究竟是如何实现线程之间独立的:

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 twor &= m;else { // reject over-represented candidatesfor (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 seedUNSAFE.putLong(t = Thread.currentThread(), SEED,r = UNSAFE.getLong(t, SEED) + GAMMA);return r;}

拆分开来看,这个方法就是将线程中偏移量为 SEED 的属性的值变为了原本的种子值加上 GAMMA,然后将这个新的种子值返回,GAMMA 的注释为 The seed increment,也就是 seed 的增量。

这篇关于初探 JUC 并发编程:ThreadLocalRandom 原理剖析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/944821

相关文章

MySQL的JDBC编程详解

《MySQL的JDBC编程详解》:本文主要介绍MySQL的JDBC编程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录前言一、前置知识1. 引入依赖2. 认识 url二、JDBC 操作流程1. JDBC 的写操作2. JDBC 的读操作总结前言本文介绍了mysq

ShardingProxy读写分离之原理、配置与实践过程

《ShardingProxy读写分离之原理、配置与实践过程》ShardingProxy是ApacheShardingSphere的数据库中间件,通过三层架构实现读写分离,解决高并发场景下数据库性能瓶... 目录一、ShardingProxy技术定位与读写分离核心价值1.1 技术定位1.2 读写分离核心价值二

深度解析Python中递归下降解析器的原理与实现

《深度解析Python中递归下降解析器的原理与实现》在编译器设计、配置文件处理和数据转换领域,递归下降解析器是最常用且最直观的解析技术,本文将详细介绍递归下降解析器的原理与实现,感兴趣的小伙伴可以跟随... 目录引言:解析器的核心价值一、递归下降解析器基础1.1 核心概念解析1.2 基本架构二、简单算术表达

深入浅出Spring中的@Autowired自动注入的工作原理及实践应用

《深入浅出Spring中的@Autowired自动注入的工作原理及实践应用》在Spring框架的学习旅程中,@Autowired无疑是一个高频出现却又让初学者头疼的注解,它看似简单,却蕴含着Sprin... 目录深入浅出Spring中的@Autowired:自动注入的奥秘什么是依赖注入?@Autowired

Web服务器-Nginx-高并发问题

《Web服务器-Nginx-高并发问题》Nginx通过事件驱动、I/O多路复用和异步非阻塞技术高效处理高并发,结合动静分离和限流策略,提升性能与稳定性... 目录前言一、架构1. 原生多进程架构2. 事件驱动模型3. IO多路复用4. 异步非阻塞 I/O5. Nginx高并发配置实战二、动静分离1. 职责2

从原理到实战解析Java Stream 的并行流性能优化

《从原理到实战解析JavaStream的并行流性能优化》本文给大家介绍JavaStream的并行流性能优化:从原理到实战的全攻略,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的... 目录一、并行流的核心原理与适用场景二、性能优化的核心策略1. 合理设置并行度:打破默认阈值2. 避免装箱

深度剖析SpringBoot日志性能提升的原因与解决

《深度剖析SpringBoot日志性能提升的原因与解决》日志记录本该是辅助工具,却为何成了性能瓶颈,SpringBoot如何用代码彻底破解日志导致的高延迟问题,感兴趣的小伙伴可以跟随小编一起学习一下... 目录前言第一章:日志性能陷阱的底层原理1.1 日志级别的“双刃剑”效应1.2 同步日志的“吞吐量杀手”

Python异步编程之await与asyncio基本用法详解

《Python异步编程之await与asyncio基本用法详解》在Python中,await和asyncio是异步编程的核心工具,用于高效处理I/O密集型任务(如网络请求、文件读写、数据库操作等),接... 目录一、核心概念二、使用场景三、基本用法1. 定义协程2. 运行协程3. 并发执行多个任务四、关键

AOP编程的基本概念与idea编辑器的配合体验过程

《AOP编程的基本概念与idea编辑器的配合体验过程》文章简要介绍了AOP基础概念,包括Before/Around通知、PointCut切入点、Advice通知体、JoinPoint连接点等,说明它们... 目录BeforeAroundAdvise — 通知PointCut — 切入点Acpect — 切面

Python中的filter() 函数的工作原理及应用技巧

《Python中的filter()函数的工作原理及应用技巧》Python的filter()函数用于筛选序列元素,返回迭代器,适合函数式编程,相比列表推导式,内存更优,尤其适用于大数据集,结合lamb... 目录前言一、基本概念基本语法二、使用方式1. 使用 lambda 函数2. 使用普通函数3. 使用 N