详细介绍了ThreadLocalRandom伪随机数生成器的原理,以及对Random的优化!
文章目录
1 伪随机数2 Random2.1 随机数的生成和局限性3 ThreadLocalRandom3.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 的实例。
@Testpublic 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();}@Testpublic 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 (!pareAndSet(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学习博客!