尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
《Java并发编程之美》第 3 章 Java 并发包中 ThreadLocalRandom 类原理剖析 阅读笔记 6
《Java并发编程之美》第 3 章 Java 并发包中 ThreadLocalRandom 类原理剖析 阅读笔记 6第二部分 Java 并发编程高级篇在第一部分中我们介绍了并发编程的基础知识而本部分则主要讲解并发包中一些主要组件的实现原理。第 3 章 Java 并发包中 ThreadLocalRandom 类原理剖析ThreadLocalRandom 类是 JDK 7 在 JUC 包下新增的随机数生成器它弥补了 Random 类在多线程下的缺陷。本章讲解为何要在 JUC 下新增该类以及该类的实现原理。3.1 Random 类及其局限性在 JDK 7 之前包括现在java.util.Random 都是使用比较广泛的随机数生成工具类而且 java.lang.Math 中的随机数生成也使用的是 java.util.Random 的实例。下面先看看 java. util.Random 的使用方法。publicclassRandomTest{publicstaticvoidmain(String[]args){//(1)创建一个默认种子的随机数生成器RandomrandomnewRandom();//(2)输出10个在05包含0不包含5之间的随机数for(inti0;i10;i){System.out.println(random.nextInt(5));}}}代码1创建一个默认随机数生成器并使用默认的种子。代码2输出 10 个在 05包含 0不包含 5之间的随机数。随机数的生成需要一个默认的种子这个种子其实是一个 long 类型的数字你可以在创建 Random 对象时通过构造函数指定如果不指定则在默认构造函数内部生成一个默认的值。有了默认的种子后如何生成随机数呢publicintnextInt(intbound){//(3)参数检查if(bound0)thrownewIllegalArgumentException(BadBound);//(4)根据老的种子生成新的种子intrnext(31);//(5)根据新的种子计算随机数...returnr;}由此可见新的随机数的生成需要两个步骤首先根据老的种子生成新的种子。然后根据新的种子来计算新的随机数。其中步骤4我们可以抽象为 seedfseed​其中 f 是一个固定的函数比如 seedfseedaseedb步骤5也可以抽象为 gseed, bound​其中 g 是一个固定的函数比如 gseed, boundint​​boundlongseed31​。在单线程情况下每次调用 nextInt 都是根据老的种子计算出新的种子这是可以保证随机数产生的随机性的。但是在多线程下多个线程可能都拿同一个老的种子去执行步骤4以计算新的种子这会导致多个线程产生的新种子是一样的由于步骤5的算法是固定的所以会导致多个线程产生相同的随机值这并不是我们想要的。所以步骤4要保证原子性也就是说当多个线程根据同一个老种子计算新种子时第一个线程的新种子被计算出来后第二个线程要丢弃自己老的种子而使用第一个线程的新种子来计算自己的新种子依此类推只有保证了这个才能保证在多线程下产生的随机数是随机的。Random 函数使用一个原子变量达到了这个效果在创建 Random 对象时初始化的种子就被保存到了种子原子变量里面下面看 next() 的代码。protectedintnext(intbits){longoldseed,nextseed;AtomicLongseedthis.seed;do{//(6)oldseedseed.get();//(7)nextseed(oldseed multiplieraddend)mask;//(8)}while(!seed.compareAndSet(oldseed,nextseed));//(9)return(int)(nextseed(48-bits));}代码6获取当前原子变量种子的值。代码7根据当前种子值计算新的种子。代码8使用 CAS 操作它使用新的种子去更新老的种子CAS 即 Compare and Swap其是 JDK 提供的非阻塞原子性操作它通过硬件保证了比较—更新操作的原子性。在多线程下可能多个线程都同时执行到了代码6​那么可能多个线程拿到的当前种子的值是同一个然后执行步骤7计算的新种子也都是一样的但是步骤8的 CAS 操作会保证只有一个线程可以更新老的种子为新的失败的线程会通过循环重新获取更新后的种子作为当前种子去计算新的种子这就解决了上面提到的问题保证了随机数的随机性。代码9使用固定算法根据新的种子计算随机数。总结每个 Random 实例里面都有一个原子性的种子变量用来记录当前的种子值当要生成新的随机数时需要根据当前种子计算新的种子并更新回原子变量。在多线程下使用单个 Random 实例生成随机数时当多个线程同时计算随机数来计算新的种子时多个线程会竞争同一个原子变量的更新操作由于原子变量的更新是 CAS 操作同时只有一个线程会成功所以会造成大量线程进行自旋重试这会降低并发性能所以 ThreadLocalRandom 应运而生。3.2 ThreadLocalRandompackagecom.yangjunbo.juc.three;importjava.util.concurrent.ThreadLocalRandom;/** * ClassName: ThreadLocalRandom * Package: com.yangjunbo.juc.three * Description: * * Author 杨钧博 * Create 2026/10/21 10:27 * Version 1.0 */publicclassThreadLocalRandomTest{publicstaticvoidmain(String[]args){//(10)获取一个随机数生成器ThreadLocalRandomrandomThreadLocalRandom.current();//(11)输出10个在05包含0不包含5之间的随机数for(inti0;i10;i){System.out.println(random.nextInt(5));}}}其中代码10调用 ThreadLocalRandom.current() 来获取当前线程的随机数生成器。下面来分析下 ThreadLocalRandom 的实现原理。从名字上看它会让我们联想到在基础篇中讲解的 ThreadLocalThreadLocal 通过让每一个线程复制一份变量使得在每个线程对变量进行操作时实际是操作自己本地内存里面的副本从而避免了对共享变量进行同步。实际上 ThreadLocalRandom 的实现也是这个原理Random 的缺点是多个线程会使用同一个原子性种子变量从而导致对原子变量更新的竞争如图 3-1 所示。图 3-1那么如果每个线程都维护一个种子变量则每个线程生成随机数时都根据自己老的种子计算新的种子并使用新种子更新老的种子再根据新种子计算随机数就不会存在竞争问题了这会大大提高并发性能。ThreadLocalRandom 原理如图 3-2 所示。图 3-23.3 源码分析首先看下 ThreadLocalRandom 的类图结构如图 3-3 所示。图 3-3从图中可以看出 ThreadLocalRandom 类继承了 Random 类并重写了 nextInt 方法在 ThreadLocalRandom 类中并没有使用继承自 Random 类的原子性种子变量。在 ThreadLocalRandom 中并没有存放具体的种子具体的种子存放在具体的调用线程的 threadLocalRandomSeed 变量里面。ThreadLocalRandom 类似于 ThreadLocal 类就是个工具类。当线程调用 ThreadLocalRandom 的 current 方法时ThreadLocalRandom 负责初始化调用线程的 threadLocalRandomSeed 变量也就是初始化种子。当调用 ThreadLocalRandom 的 nextInt 方法时实际上是获取当前线程的 threadLocalRandomSeed 变量作为当前种子来计算新的种子然后更新新的种子到当前线程的 threadLocalRandomSeed 变量而后再根据新种子并使用具体算法计算随机数。这里需要注意的是threadLocalRandomSeed 变量就是 Thread 类里面的一个普通 long 变量它并不是原子性变量。其实道理很简单因为这个变量是线程级别的所以根本不需要使用原子性变量。其中 seeder 和 probeGenerator 是两个原子性变量在初始化调用线程的种子和探针变量时会用到它们每个线程只会使用一次。另外变量 instance 是 ThreadLocalRandom 的一个实例该变量是 static 的。当多线程通过 ThreadLocalRandom 的 current 方法获取 ThreadLocalRandom 的实例时其实获取的是同一个实例。但是由于具体的种子是存放在线程里面的所以在 ThreadLocalRandom 的实例里面只包含与线程无关的通用算法所以它是线程安全的。下面看看 ThreadLocalRandom 的主要代码的实现逻辑。1Unsafe 机制privatestaticfinalsun.misc.UnsafeUNSAFE;privatestaticfinallongSEED;privatestaticfinallongPROBE;privatestaticfinallongSECONDARY;static{try{//获取unsafe实例UNSAFEsun.misc.Unsafe.getUnsafe();Class?tkThread.class;//获取Thread类里面threadLocalRandomSeed变量在Thread实例里面的偏移量SEEDUNSAFE.objectFieldOffset(tk.getDeclaredField(threadLocalRandomSeed));//获取Thread类里面threadLocalRandomProbe变量在Thread实例里面的偏移量PROBEUNSAFE.objectFieldOffset(tk.getDeclaredField(threadLocalRandomProbe));//获取Thread类里面threadLocalRandomSecondarySeed变量在Thread实例里面的偏移量这个值在后面讲解LongAdder时会用到SECONDARYUNSAFE.objectFieldOffset(tk.getDeclaredField(threadLocalRandomSecondarySeed));}catch(Exceptione){thrownewError(e);}}2ThreadLocalRandom current() 方法该方法获取 ThreadLocalRandom 实例并初始化调用线程中的 threadLocalRandomSeed 和 threadLocalRandomProbe 变量。staticfinalThreadLocalRandominstancenewThreadLocalRandom();publicstaticThreadLocalRandomcurrent(){//(12)if(UNSAFE.getInt(Thread.currentThread(),PROBE)0)//(13)localInit();//(14)returninstance;}staticfinalvoidlocalInit(){intpprobeGenerator.addAndGet(PROBE_INCREMENT);intprobe(p0)?1:p;// skip 0longseedmix64(seeder.getAndAdd(SEEDER_INCREMENT));ThreadtThread.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 实例。3int nextInt(int bound) 方法计算当前线程的下一个随机数。publicintnextInt(intbound){//(15)参数校验if(bound0)thrownewIllegalArgumentException(BadBound);//(16) 根据当前线程中的种子计算新种子intrmix32(nextSeed());//(17)根据新种子和bound计算随机数intmbound-1;if((boundm)0)// power of tworm;else{// reject over-represented candidatesfor(intur1;um-(ru%bound)0;umix32(nextSeed())1);}returnr;}如上代码的逻辑步骤与 Random 相似我们重点看下 nextSeed() 方法。finallongnextSeed(){Threadt;longr;//UNSAFE.putLong(tThread.currentThread(),SEED,rUNSAFE.getLong(t,SEED)GAMMA);returnr;}在如上代码中首先使用 r UNSAFE.getLongt, SEED获取当前线程中 threadLocalRandomSeed 变量的值然后在种子的基础上累加 GAMMA 值作为新种子而后使用 UNSAFE 的 putLong 方法把新种子放入当前线程的 threadLocalRandomSeed 变量中。3.4 总结本章首先讲解了 Random 的实现原理以及 Random 在多线程下需要竞争种子原子变量更新操作的缺点从而引出 ThreadLocalRandom 类。ThreadLocalRandom 使用 ThreadLocal 的原理让每个线程都持有一个本地的种子变量该种子变量只有在使用随机数时才会被初始化。在多线程下计算新种子时是根据自己线程内维护的种子变量进行更新从而避免了竞争。
RELATED

相关推荐

第3篇 · 使用 PyQt5 实现 2048 桌面版游戏:MVC 架构与撤销和重做

第3篇 · 使用 PyQt5 实现 2048 桌面版游戏:MVC 架构与撤销和重做

第3篇 使用 PyQt5 实现 2048 桌面版游戏:MVC 架构与撤销和重做 前两篇为纯 Python 实现:第 1 篇的数学模型、第 2 篇的带历史记录的控制台版。控制台版适合验证逻辑,但交互体验并不好:每执行一步都要 cls 清屏、光标闪烁&#x…

📅 2026/10/22 21:00:26
Linux进程篇(六):穿过底层看本质,深入理解底层进程切换与 O(1) 调度算法

Linux进程篇(六):穿过底层看本质,深入理解底层进程切换与 O(1) 调度算法

目录 一、 进程切换与 CPU 寄存器原理 1.1 死循环进程如何运行?——深入理解时间片 1.1.1 现代操作系统的公平调度 1.2 聊聊 CPU 寄存器与临时数据 1.2.1 寄存器的诞生背景与核心作用 1.2.2 常见的核心寄存器分类 1.3 核心结论:区分“空间”与“内…

📅 2026/10/22 21:00:26
人机协同:用 interrupt 把控制权交给你

人机协同:用 interrupt 把控制权交给你

个人主页&#xff1a;> for_ever_love__ <&#xff08;欢迎各位大佬莅临&#x1f60a;&#xff09; 其他栏目: > 大模型开发从0到1 < 其他栏目: > iOS项目总结大全 < 其他栏目: > 我想学python了 < 其他栏目: > iOS UI < 文章目录人机协同&#…

📅 2026/10/22 20:55:26
MORE NEWS

更多资讯

📰

多Agent系统中Skill统一管理:SkillHub与AgentForge实战指南

1. 项目概述&#xff1a;当一个 skill 在多 agent 系统里“分身三处”时&#xff0c;你真的知道它在哪执行、用的是哪版代码、改了会不会崩吗&#xff1f;“同一个 skill 存了三份”——这句话不是段子&#xff0c;是我在某跨平台智能体协作系统上线第三周时&#xff0c;盯着日…

📰

基于YOLOv8的活体检测系统:数据集构建、模型训练与Web部署实战

做活体检测系统这个选题&#xff0c;我是很有感触的。这两年人脸识别已经烂大街了&#xff0c;门禁、考勤、支付、手机解锁全在用人脸&#xff0c;可真正愿意把“活体”这两个字做实、做细的项目却不多。很多人拿着照片就能把门禁骗过去&#xff0c;拿着平板重放一段录好的视频…

📰

基于YOLOv8的人脸活体检测系统:从数据标注到Web部署全流程

1. 项目是怎么来的&#xff1a;这套系统到底解决什么问题 人脸活体检测这个东西&#xff0c;说直白一点&#xff0c;就是让机器分辨“镜头前面是一张真脸&#xff0c;还是一张照片、一段视频、一个3D面具”。刷脸支付、闸机门禁、远程实名认证&#xff0c;这些场景全依赖它兜底…

📰

端侧AI推理引擎LiteRT深度解析:从架构分层到量化优化实战

1. 端侧AI的工程瓶颈&#xff1a;纸面算力与真实功耗之间的鸿沟1.1 云端方案为何在端侧“水土不服”我一直觉得&#xff0c;端侧AI最迷惑人的地方在于&#xff1a;模型在服务器上跑得很好&#xff0c;各项跑分都很漂亮&#xff0c;但一旦搬上手机或边缘盒子&#xff0c;延迟翻倍…

📰

AI开发工具链全解析:从编码辅助到模型部署的工程实践

1. 从编码辅助到模型部署&#xff1a;AI开发工具链的全景俯瞰这两年只要在一线写过代码&#xff0c;就很难绕过AI开发工具这四个字。Git提交历史里塞满了一行行“AI生成注释”“根据AI建议修改”的commit&#xff0c;代码评审的评论区里多了“这个函数AI这么写其实有隐患”的讨…

📰

Ubuntu系统盘制作与安装全攻略:从镜像写入到双系统引导修复

1. 为什么值得花时间把系统盘制作这件事搞明白 很多人第一次接触 Ubuntu 安装&#xff0c;卡住的地方往往不是系统本身&#xff0c;而是"盘没做对"。我见过太多人下载完 ISO 镜像&#xff0c;双击打开&#xff0c;把里面的文件一股脑拖进 U 盘&#xff0c;然后插上电…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬