Java ThreadLocalRandom 伪随机数生成器的源码深度解析与应用
详细介绍了ThreadLocalRandom伪随机数生成器的原理,以及对Random的优化!
文章目录
- 1 伪随机数
- 2 Random
- 2.1 随机数的生成和局限性
- 3 ThreadLocalRandom
- 3.1 概述
- 3.2 current方法
- 3.3 nextInt方法
- 3.4 总结
1 伪随机数
简单的了解一下伪随机数的概念。
通过固定算法产生的随机数都是伪随机数,Java语言的随机数生成器或者说整个计算机中的随机数函数,生成的都是伪随机数。只有通过真实的随机事件产生的随机数才是真随机数。比如,通过机器的硬件、CPU温度、当天天气、噪音等真随机事件产生随机数,如果想要产生真随机数,那么需要一定的物理手段,开销极大,或许得不偿失!因此目前的随机数都是通过一个非常复杂的算法计算出来的!
Random生成的随机数都是伪随机数,是由可确定的函数(常用线性同余算法),通过一个seed(种子,常用时钟),产生的伪随机数。这意味着:如果知道了种子,或者已经产生的随机数,都可能获得接下来随机数序列的信息(可预测性)。
我们对两个Random对象指定相同的种子,测试结果如下:
//新建random对象,指定种子
Random random1 = new Random(100);
//生成10个[0,100)的伪随机数
for (int i = 0; i < 10; i++) {System.out.print(random1.nextInt(100) + " ");
}
System.out.println();
System.out.println("===============");
//新建random对象,指定种子
Random random2 = new Random(100);
//生成10个[0,100)的伪随机数
for (int i = 0; i < 10; i++) {System.out.print(random2.nextInt(100) + " ");
}
由于算法一样,当时用了相同的seed(种子),生成了一样的伪随机数,结果为:
15 50 74 88 91 66 36 88 23 13
===============
15 50 74 88 91 66 36 88 23 13
这里不讨论过多的真随机数和伪随机数,主要是讲解Random和新的ThreadLocalRandom这个两个类的性能差异的原理。
2 Random
Random是非常常见的伪随机数生成工具类,而且java.Iang.Math 中的伪随机数生成使用的也是Random 的实例。
@Test
public void test1() {//新建random对象,使用默认种子Random random = new Random();///生成10个[0,100)的伪随机数for (int i = 0; i < 10; i++) {System.out.print(random.nextInt(100) + " ");}double random1 = Math.random();
}@Test
public void test4() {//Math.random()用于生成[0.0,1.0)之间的double正数for (int i = 0; i < 10; i++) {System.out.println(Math.random());}//Math.random()内部也是使用的Random类//public static double random() {// return RandomNumberGeneratorHolder.randomNumberGenerator.nextDouble();//}//private static final class RandomNumberGeneratorHolder {// static final Random randomNumberGenerator = new Random();//}
}
2.1 随机数的生成和局限性
随机数的生成需要一个默认的long类型的“种子”,可以在创建Random 对象时通过构造函数指定,如果不指定则在默认构造函数内部生成一个默认的值。这个种子用于生成第一个随机数,并且生成下一个随机数种子,之后的以后产生的随机数都与前一个随机数种子有关。
随机数的生成逻辑主要在nextint方法中。主要分为两步:
- 根据老的send(种子)生成新的种子,并且初步生成随机数r;
- 对伪随机数r使用固定算法进行进一步处理,最后返回。
/*** @param bound 要返回的随机数的上限范围。必须为正数。* @return 返回位于[0, bound]之间的伪随机整数* @throws IllegalArgumentException 如果 n 不是正数*/
public int nextInt(int bound) {//bound小于等于0则抛出异常if (bound <= 0)throw new IllegalArgumentException(BadBound);//初步生成伪随机数r以及更新下一个种子值int r = next(31);//对r使用固定算法进行进一步处理int m = bound - 1;if ((bound & m) == 0) // i.e., bound is a power of 2r = (int) ((bound * (long) r) >> 31);else {for (int u = r;u - (r = u % bound) + m < 0;u = next(31));}return r;
}
在多线程环境下Random也能正常使用,主要是得益于next方法中生成下一个种子的CAS处理:
/*** @param bits 随机数位* @return 初步生的成随机数,以及下一个种子*/
protected int next(int bits) {long oldseed, nextseed;//获取此时的seed,可以看到种子变量是使用AtomicLong变量来保存的AtomicLong seed = this.seed;do {//获取此时最新的seed值oldseedoldseed = seed.get();//计算下一个seed值nextseednextseed = (oldseed * multiplier + addend) & mask;//尝试CAS的将seed从oldseed更新为nextseed值,成功后返回,失败重试//这里保证了多线程下数据的安全性,即每一个线程都不会获得错误或者重复的随机数} while (!seed.compareAndSet(oldseed, nextseed));//根据新种子计算出随机数并返回return (int) (nextseed >>> (48 - bits));
}/*** 种子*/
private final AtomicLong seed;/*** 构造器*/
public Random() {//调用有参构造器,采用时钟System.nanoTime()进行计算作为初始种子this(seedUniquifier() ^ System.nanoTime());
}
CAS 操作保证每次只有一个线程可以获取并成功更新种子,失败的线程则需要自旋重试,自旋的时候又会获取最新的种子。因此每一个线程最终总会计算出不同的种子,保证了多线程环境下的数据安全。初始种子可以自己指定,没有参数的构造器中采用当前时间戳来计算种子。
在多线程下使用单个Random 实例生成随机数时,可能会存在多个线程同时尝试CAS更新同一个种子的操作,CAS保证并发操作只有一个线程能够成功,其他线程则会自旋重试,保证了线程安全。但是如果大量线程频繁的尝试生成随机数,那么可能会造成大量线程因为失败而自旋重试,降低并发性能,消耗CPU资源。因此在JDK1.7的时候出现了ThreadLocalRandom ,就是为了解决这个问题!
3 ThreadLocalRandom
3.1 概述
public class ThreadLocalRandom
extends Random
ThreadLocalRandom 类来自于JDK 1.7的JUC包,继承并扩展了Random类,主要是弥补了Random类在多线程下由于竞争同一个seed造成性能低下的缺陷。
简单的说就是一个线程隔离的随机数生成器,使用了ThreadLocal的机制(ThreadLocal源码深度解析与应用案例)。因此ThreadLocalRandom非常适合在并发程序中使用,比如在线程池中需要使用随机数的时候时!
不同于Random多个线程共享一个seed种子,每个线程维护自己的一个普通long类型的种子变量,就是Thread中的threadLocalRandomSeed变量,每个线程生成随机数时候根据自己老的种子计算新的种子,并使用新种子更新老的种子,然后根据新种子计算随机数,这样就不会存在竞争问题,这会大大提高并发性能。ThreadLocalRandom类似于ThreadLocal,就像是一个工具,对外提供API方法,实际的种子数据都是每一个线程自己保存。
ThreadLocalRandom内部的属性操作使用到了Unsafe类,这是一个根据字段偏移量来操作对象的字段的类,是JUC包实现的底层基石类,Unsafe直接操作JVM内存,效率更高,同时也提供了CAS实现的Java本地接口!
简单使用方法如下,我们不需要像Random那样建立一个全局ThreadLocalRandom对象,而是在当前线程中获取即可!
public static void main(String[] args) {Run run = new Run();new Thread(run).start();new Thread(run).start();
}static class Run implements Runnable {@Overridepublic void run() {//获取ThreadLocalRandom伪随机数生成器ThreadLocalRandom current = ThreadLocalRandom.current();///生成10个[0,100)的伪随机数for (int i = 0; i < 10; i++) {System.out.print(current.nextInt(100) + " ");}System.out.println(Thread.currentThread().getName());}
}
3.2 current方法
public static ThreadLocalRandom current()
ThreadLocalRandom的构造函数是私有的,只能使用静态工厂方法ThreadLocalRandom.current()返回它的实例–instance。instance 是ThreadLocalRandom 的一个static final属性,该变量是static 的。在ThreadLocalRandom类加载的初始化阶段就初始化好了的,因此实际上不同的线程的current()方法返回的是同一个对象,这是单例模式的应用。
当线程调用ThreadLocalRandom.current静态方法时候,ThreadLocalRandom就会初始化调用线程的threadLocalRandomSeed变量,也就是当前线程的初始化种子。另外返回的ThreadLocalRandom对象实际上是在ThreadLocalRandom类加载的初始化阶段就初始化好了的,因此实际上不同的线程的current方法返回的是同一个对象,这是一个工作方法。
该静态方法获取 ThreadLocalRandom 实例,并初始化调用线程中 threadLocalRandomSeed 和 threadLocalRandomProbe 变量。源码如下:
//Thread中保存的变量,只能在当前线程中被访问,使用了缓存填充(注解方式)来避免伪共享。/*** 线程本地随机的当前种子。*/
@sun.misc.Contended("tlr")
long threadLocalRandomSeed;/*** 探测哈希值;如果线程本地随机种子被初始化,那么该值也非0。使用缓存填充(注解方式)来避免伪共享。*/
@sun.misc.Contended("tlr")
int threadLocalRandomProbe;//ThreadLocalRandom中保存的变量和方法/*** 单例对象,在类加载的时候就被初始化了,后续的线程每次都是获取同一个实例*/
static final ThreadLocalRandom instance = new ThreadLocalRandom();/*** @return 返回当前线程的 ThreadLocalRandom*/
public static ThreadLocalRandom current() {//使用UNSAFE获取当前线程对象的PROBE偏移量处的int类型的值(PROBE就是static静态块中获取的threadLocalRandomProbe变量的偏移量)//如果等于0(每一个线程的threadLocalRandomProbe变量在默认情况下是没有初始化的,默认值就是0)//说明当前线程第一次调用 ThreadLocalRandom 的 current 方法,那么就需要调用 localInit 方法计算当前线程的初始化种子变量。if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)//初始化种子localInit();//返回单例return instance;
}/*** 初始化种子*/
static final void localInit() {int p = probeGenerator.addAndGet(PROBE_INCREMENT);//跳过0int probe = (p == 0) ? 1 : p; //long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));//获取当前线程Thread t = Thread.currentThread();//设置当前线程的初始化种子,SEED就是static静态块中获取的threadLocalRandomSeed变量的偏移量,值就是seedUNSAFE.putLong(t, SEED, seed);//设置当前线程的初始化探针,PROBE就是static静态块中获取的threadLocalRandomProbe变量的偏移量,值就是probeUNSAFE.putInt(t, PROBE, probe);
}/*** 该字段用于计算当前线程中 threadLocalRandomProbe 的初始化值* 这是一个static final变量,但是它的值却是可变的,多线程共享*/
private static final AtomicInteger probeGenerator = new AtomicInteger();
/*** 初始化threadLocalRandomProbe值的默认增量,每个线程初始化时调用一次,就增加0x9e3779b9,值为-1640531527* 这个数字的得来是 2^32 除以一个常数,而这个常数就是传说中的黄金比例 1.6180339887* 目的是为了让随机数取值更加均匀:https://www.javaspecialists.eu/archive/Issue164.html(0x61c88647就是1640531527)*/
private static final int PROBE_INCREMENT = 0x9e3779b9;
/*** 该字段用于计算初始化SEED值,这是一个static final常量,但是它的值却是可变的*/
private static final AtomicLong seeder = new AtomicLong(initialSeed());
/*** 初始化seeder值的默认增量,每个线程初始化时调用一次,就会增加SEEDER_INCREMENT*/
private static final long SEEDER_INCREMENT = 0xbb67ae8584caa73bL;
3.3 nextInt方法
public int nextInt(int bound)
返回0(包括)和指定的绑定(排除)之间的伪随机 int值。
当调用ThreadLocalRandom.nextInt方法时候,实际上是获取当前线程的threadLocalRandomSeed变量作为当前种子来计算新的种子,然后更新新的种子到当前线程的threadLocalRandomSeed变量,然后在根据新种子和具体算法计算随机数。
/*** @param bound 伪随机数上限* @return 获取[0, bound)的伪随机整数*/
public int nextInt(int bound) {//bound范围校验if (bound <= 0)throw new IllegalArgumentException(BadBound);//根据当前线程中的种子计算新种子int r = mix32(nextSeed());//根据新种子和bound计算随机数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;
}/*** 用于根据当前种子,计算和更新下一个种子** @return*/
final long nextSeed() {Thread t;long r; // read and update per-thread seed//更新计算出的种子,即更新当前线程的threadLocalRandomSeed变量UNSAFE.putLong(t = Thread.currentThread(), SEED,//计算新种子,为原种子+增量r = UNSAFE.getLong(t, SEED) + GAMMA);return r;
}/*** 种子增量*/
private static final long GAMMA = 0x9e3779b97f4a7c15L;
3.4 总结
ThreadLocalRandom 使用ThreadLocal的原理,让每个线程都持有一个本地的种子变量,该种子变量只有在使用随机数时才会被初始化。在多线程下计算新种子时每一个线程是根据自己内部维护的种子变量进行更新,从而避免了竞争,提升了效率!
相关文章:
- JUC—Unsafe类的原理详解与使用案例
- ThreadLocal源码深度解析与应用案例
如有需要交流,或者文章有误,请直接留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!
Java ThreadLocalRandom 伪随机数生成器的源码深度解析与应用相关推荐
- Java LockSupport以及park、unpark方法源码深度解析
介绍了JUC中的LockSupport阻塞工具以及park.unpark方法的底层原理,从Java层面深入至JVM层面. 文章目录 1 LockSupport的概述 2 LockSupport的特征和 ...
- Spring源码深度解析(郝佳)-学习-源码解析-基于注解bean定义(一)
我们在之前的博客 Spring源码深度解析(郝佳)-学习-ASM 类字节码解析 简单的对字节码结构进行了分析,今天我们站在前面的基础上对Spring中类注解的读取,并创建BeanDefinition做 ...
- 《Spring源码深度解析 郝佳 第2版》AOP
往期博客 <Spring源码深度解析 郝佳 第2版>容器的基本实现与XML文件的加载 <Spring源码深度解析 郝佳 第2版>XML标签的解析 <Spring源码深度解 ...
- 《Spring源码深度解析 郝佳 第2版》SpringBoot体系分析、Starter的原理
往期博客 <Spring源码深度解析 郝佳 第2版>容器的基本实现与XML文件的加载 <Spring源码深度解析 郝佳 第2版>XML标签的解析 <Spring源码深度解 ...
- Spring源码深度解析(郝佳)-学习-源码解析-创建AOP静态代理实现(八)
继上一篇博客,我们继续来分析下面示例的 Spring 静态代理源码实现. 静态 AOP使用示例 加载时织入(Load -Time WEaving,LTW) 指的是在虚拟机载入字节码时动态织入 Aspe ...
- Spring源码深度解析(郝佳)-Spring 常用注解使用及源码解析
我们在看Spring Boot源码时,经常会看到一些配置类中使用了注解,本身配置类的逻辑就比较复杂了,再加上一些注解在里面,让我们阅读源码更加难解释了,因此,这篇博客主要对配置类上的一些注解的使用 ...
- Spring源码深度解析(郝佳)-学习-源码解析-基于注解切面解析(一)
我们知道,使用面积对象编程(OOP) 有一些弊端,当需要为多个不具有继承关系的对象引入同一个公共的行为时,例如日志,安全检测等,我们只有在每个对象引用公共的行为,这样程序中能产生大量的重复代码,程序就 ...
- 《Spring源码深度解析 郝佳 第2版》JDBC、MyBatis原理
往期博客 <Spring源码深度解析 郝佳 第2版>容器的基本实现与XML文件的加载 <Spring源码深度解析 郝佳 第2版>XML标签的解析 <Spring源码深度解 ...
- Spring源码深度解析(郝佳)-学习-源码解析-Spring整合MyBatis
了解了MyBatis的单独使用过程之后,我们再来看看它也Spring整合的使用方式,比对之前的示例来找出Spring究竟为我们做了什么操作,哪些操作简化了程序开发. 准备spring71.xml &l ...
- Spring源码深度解析(郝佳)-学习-源码解析-Spring MVC(三)-Controller 解析
在之前的博客中Spring源码深度解析(郝佳)-学习-源码解析-Spring MVC(一),己经对 Spring MVC 的框架做了详细的分析,但是有一个问题,发现举的例子不常用,因为我们在实际开发项 ...
最新文章
- try-catch-finally中的4个巨坑,老程序员也搞不定!
- Spring.NET 学习总结
- 在2019年比较云计算服务时需要问的6个问题
- python sklearn.preprocessing.standardScaler 的transform和fit_transform方法
- socket,TCP/IP的理解(转)
- JFinal 1.5 发布,JAVA极速WEB+ORM框架
- 如何实现一个符合泊松分布的即时消息发生器?
- mysql怎么跳出while循环_mysql while,loop,repeat循环,符合条件跳出循环
- 2022年中国全屋智能行业研究白皮书
- Pycharm 安装
- Create Material by BDC and BAPI
- 通过修改控制文件scn推进数据库scn
- 公众号H5跳转小程序
- 初中数学可以用计算机吗,中学生利用手机电脑学习数学可行吗?
- 关于瞬时功率的无功功率和有功功率,在不同坐标系下的验证
- 小程序 选择地区(地区选择器)
- 怎样删除Github中的项目
- Oracle一备份内存就占满卡死,rman备份占用内存问题
- eDP接口信号组成认识
- G003-186-04
热门文章
- 对日外包总结-泛泛而谈
- 【大数据技术详解】搭建redis集群服务的步骤和配置以及解决创建集群时会遇到的错误:NodeX replied with error:ERRInvalid node address specified
- EDG:相信奇迹的人,本身就是奇迹。
- 连续潜在变量---主成分分析
- Linux查看服务器SN序列码
- python归一化nan加扰动_标准化和归一化对机器学习经典模型的影响
- 微软2016校园招聘4月在线笔试 总结
- 新浪短网址生成java_新浪短网址(T.cn)/腾讯短链接(Url.cn)在线生成以及API接口申请的教程...
- PTA 1072 开学寄语
- Cesium 添加边界墙边界线