Random(一)高并发问题,ThreadLocalRandom源码解析

1.什么是伪随机数?

伪随机数: 通过固定算法产生的随机数就是伪随机数。

  • 只有通过真实的随机时间产生的随机数才是真正的随机数。比如:通过机器的硬件、CPU温度、当天天气、噪声等真随机事件产生随机数。
  • 如果想产生真的随机数,需要一定的物理手段,开销极大,得不偿失。
  • 目前 Java 语言或者说整个计算机行业中使用的随机数函数,都是通过不同的算法计算出来的伪随机数。

2.Random

Random: 是非常常见的伪随机数生成类,种子默认使用系统的时间,生成结果可预测,安全性不高,线程安全。

  • java.lang.Math 中的伪随机数生成使用的就是 Random 实例。

2.1 使用示例

代码测试:

public static void main(String[] args) {
    // Random
    Random random1 = new Random(100);
    Random random2 = new Random(100);
    print(random1);
    print(random2);
}

private static void print(Random random) {
    for (int i = 0; i < 10; i++) {
        System.out.print(random.nextInt(100) + ",");
    }
    System.out.println();
}

执行结果:

15,50,74,88,91,66,36,88,23,13,
15,50,74,88,91,66,36,88,23,13,

2.2 什么种子重复,随机数会重复?

Random 随机数的生成需要一个默认的 long 类型的种子,用于生成一个新的随机种子和随机数,之后产生的随机数都依赖于前一个随机数种子。所以当第一个种子重复时,后面的随机数都会重复

2.3 nextInt() 源码分析

public int nextInt(int bound)

用法: 返回 0(包括)和指定的绑定值(排除)之间的伪随机数 int 值。

当调用 Random.nextInt() 时,实际上时获取 Random 实例的 seed 变量作为当前种子来计算新的种子,然后更新新的种子到当前线程的 threadLocalRandomSeed 变量上,最后根据新种子的值和具体算法计算随机数。

/**
 * @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)
        r = (int) ((bound * (long) r) >> 31);
    else {
        for (int u = r;
             u - (r = u % bound) + m < 0;
             u = next(31))
            ;
    }
    return r;
}

2.4 线程安全的实现

Random 在多线程环境下也可以正常使用,主要得益于 next 方法中生成下一个种子的 CAS 处理:

/**
 * 种子(可以看到种子变量是使用 AtomicLong 变量来保存的,方便后续执行 CAS 操作)
 */
private final AtomicLong seed;
/**
 * @param bits 随机位数
 * @return 初步生成的随机数,以及下一个种子
 */
protected int next(int bits) {
    // 定义旧种子,下一个种子
    long oldseed, nextseed;
    // 获取此时的 seed
    AtomicLong seed = this.seed;
    do {
        // 获得此时最新的 seed 值 oldseed 
        oldseed = seed.get();
        // 计算下一个 seed 值 nextseed
        nextseed = (oldseed * multiplier + addend) & mask;
        // 循环尝试 CAS,将 seed 从 oldseed 更新为 nextseed 值,成功后返回,失败重试
        // 如果 seed 的值还是为 oldseed,就用 nextseed 替换掉,并且返回 true,退出 while 循环
        // 如果已经不是 oldseed 了,就返回 false,继续循环
    } while (!seed.compareAndSet(oldseed, nextseed));
    
    // 根据新种子计算出随机数并返回
    return (int)(nextseed >>> (48 - bits));
}

流程图如下:

在这里插入图片描述

  • CAS 操作保证每次只有一个线程可以获取并成功更新种子,失败的线程则需要自选重试,自旋的时候又会获取最新的种子。
  • 因此每一个线程最终总会计算出不同的种子,保证了多线程环境下的数据安全。

2.5 高并发问题

问题简述: 高并发使用 Random 或 SecureRandom 会变慢。

  • 在多线程下使用单个 Random 实例生成随机数时,可能会存在多个线程同时尝试 CAS 更新同一个种子的操作。
  • CAS 保证并发操作只有一个线程能够成功,其他线程会自旋重试,保证了线程安全。
  • 但是如果大量线程频繁的尝试生成随机数,那么可能会造成大量线程因为失败而自旋重试,降低并发性能,消耗 CPU 资源。
  • 因此在 JDK1.7 中出现了 ThreadLocalRandom,就是为了解决这个问题。

3.ThreadLocalRandom

ThreadLocalRandom: 位于 jdk1.7JUC 包,继承并扩展了 Random 类,弥补了多线程下 CAS 共同竞争同一个 seed 导致性能低下的缺陷,多线程下推荐使用

  • ThreadLocalRandom 使用 ThreadLoca 的原理,让每个线程内持有一个本地的种子变量,该种子变量只有在使用随机数时候才会被初始化,多线程下计算新种子时是每一个线程是根据自己内部维护自己线程维护的种子变量进行更新,从而避免了竞争。
  • ThreadLocalRandom 内部的属性操作使用到了 Unsafe 类,这是一个根据字段偏移量来操作对象字段的类,是 JUC 包实现的底层基石类,Unsafe 直接操作 JVM 内存,效率更高,同时也提供了 CAS 实现的 Java 本地接口。

3.1 使用示例

代码实现:

public static void main(String[] args) {
    // 获取 ThreadLocalRandom 伪随机数生成器
    ThreadLocalRandom current = ThreadLocalRandom.current();
    // 生成10个[0,100)的伪随机数
    for (int i = 0; i < 100; i++) {
        System.out.print(current.nextInt(100) + ",");
    }
}

执行结果:

64,15,98,14,68,6,……,73,64,

3.2 current() 源码解析

public static ThreadLocalRandom current()

  • ThreadLocalRandom 的构造函数是私有的,只能使用静态工厂方法 current() 返回它的实例——instance。instance 是ThreadLocalRandom 的一个 static final 属性,该变量是 static 的。在 ThreadLocalRandom 类加载的初始化阶段就初始化好了的,因此不同线程的 current() 方法实际返回的是一个对象,这是单例模式的应用

  • 所以当线程调用 ThreadLocalRandom.current() 静态方法的时候,真正的作用并不是创建实例,而是初始化调用线程的 threadLocalRandomSeed 变量,也就是当前线程的初始化种子,和 threadLocalRandomProbe 变量,并返回之前已经初始化好的实例

3.2.1 Thread中保存的变量:
  • 为了应对线程竞争,Java 中有一个 ThreadLocal 类,为每一个线程分配了一个独立的,互不相干的存储空间。ThreadLocal 的实现依赖于 Thread 对象的 ThreadLocal.ThreadLocalMap threadLocals 成员字段。

  • 与之类似,为了让随机数生成器只访问本地线程数据,从而避免竞争,在 Thread 中,又增加了 3 个成员变量。

  • 这 3 个成员变量只能在当前线程中被访问,并且使用了 @sun.misc.Contended 注解这种缓存填充来避免伪共享。

    什么是伪共享?https://blog.csdn.net/qq_33204709/article/details/129229353

/**
 * 线程本地随机的当前种子。
 */
@sun.misc.Contended("tlr")
long threadLocalRandomSeed;

/**
 * 探测哈希值;如果线程本地随机种子被初始化,那么该值也非0。使用缓存填充(注解方式)来避免伪共享。
 */
@sun.misc.Contended("tlr")
int threadLocalRandomProbe;

/**
 * 从公共线程本地随机序列中分离的二级种子。
 */
@sun.misc.Contended("tlr")
int threadLocalRandomSecondarySeed;

这3个字段作为 Thread 类的成员,便自然和每一个 Thread 对象牢牢地捆绑在一起,因 此成了名副其实的 ThreadLocal 变量,而依赖这几个变量实现的随机数生成器,也就成了 ThreadLocalRandom。

3.2.2 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);
    //跳过0
    int probe = (p == 0) ? 1 : p; //
    long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));
    //获取当前线程
    Thread t = Thread.currentThread();
    //设置当前线程的初始化种子,SEED就是static静态块中获取的threadLocalRandomSeed变量的偏移量,值就是seed
    UNSAFE.putLong(t, SEED, seed);
    //设置当前线程的初始化探针,PROBE就是static静态块中获取的threadLocalRandomProbe变量的偏移量,值就是probe
    UNSAFE.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.2 nextInt() 源码分析

public int nextInt(int bound)

用法: 返回 0(包括)和指定的绑定值(排除)之间的伪随机数 int 值。

当调用 ThreadLocaRandom.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 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;
}

/**
 * 用于根据当前种子,计算和更新下一个种子
 *
 * @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;

4.SecureRandom

SecureRandom: 伪随机数,可以理解为 Random 的安全升级版,它的种子选取比较多,主要有:时间,cpu,使用情况,点击事件等一些种子,安全性高。

相同点: 除 ThreadLoalRandom 不能指定种子外,Random 和 SecureRandom 在种子相同时,产生的随机数相同。

代码测试:

public static void main(String[] args) {
    // SecureRandom
    SecureRandom secureRandom1 =new SecureRandom("abcd".getBytes());
    SecureRandom secureRandom2 =new SecureRandom("abcd".getBytes());
    print(secureRandom1);
    print(secureRandom2);
}

/** 打印 */
private static void print(Random random) {
    for (int i = 0; i < 10; i++) {
        System.out.print(random.nextInt(100) + ",");
    }
    System.out.println();
}

执行结果:

15,50,74,88,91,66,36,88,23,13,
15,50,74,88,91,66,36,88,23,13,

34,44,40,30,1,73,49,4,37,56,
34,44,40,30,1,73,49,4,37,56,

5.关于 Random 的常见面试题

Q:Random是不是线程安全的?
A: Random是线程安全的,但是多线程下可能性能比较低。

Q:ThreadLocalRandom为什么这么快?
A: 其实这个看下源码就知道了,因为Random用了很多 CAS 的操作,ThreadLocalRandom 根本没有用到。

Q:为什么在高强度要求的情况下,不要用Random?
A: 特别是在生成验证码的情况下,不要使用Random,因为它是线性可预测的。记得有个新闻说的是一个赌博网站,为了说明其公平,公开的它的源代码,结果因为随机数可预测漏洞被攻击了。所以在安全性要求比较高的场合,应当使用SecureRandom。
参考:http://news.cnblogs.com/n/206074/

Q:从理论上来说计算机产生的随机数都是伪随机数,那么如何产生高强度的随机数?
A: 产生高强度的随机数,有两个重要的因素:种子和算法。当然算法是可以有很多的,但是如何选择种子是非常关键的因素。如Random,它的种子是System.currentTimeMillis(),所以它的随机数都是可预测的。那么如何得到一个近似随机的种子?这里有一个很别致的思路:收集计算机的各种信息,如键盘输入时间,CPU时钟,内存使用状态,硬盘空闲空间,IO延时,进程数量,线程数量等信息,来得到一个近似随机的种子。这样的话,除了理论上有破解的可能,实际上基本没有被破解的可能。而事实上,现在的高强度的随机数生成器都是这样实现的。
比如Windows下的随机数生成器:cryptGenRandom 函数 (wincrypt.h)
http://msdn.microsoft.com/en-us/library/aa379942%28VS.85%29.aspx
Linux下的 /dev/random:
http://zh.wikipedia.org/wiki//dev/random
据SecureRandom的Java doc,说到在类unix系统下,有可能是利用 /dev/random,来实现的。

其它的一些有意思的东东:
最快的安全性要求不高的生成UUID的方法(注意,强度不高,有可能会重复):

new UUID(ThreadLocalRandom.current().nextLong(), ThreadLocalRandom.current().nextLong());

整理完毕,完结撒花~





参考地址:

1.Random在高并发下的缺陷以及JUC对其的优化 ,https://www.cnblogs.com/CodeBear/p/10748407.html

2.Java ThreadLocalRandom 伪随机数生成器的源码深度解析与应用,https://blog.csdn.net/weixin_43767015/article/details/108121269

3.Java中的随机数生成器:Random,ThreadLocalRandom,SecureRandom(转),https://www.cnblogs.com/softidea/p/4006518.html

4.并发情况下,你还在用Random生成随机数?

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不愿放下技术的小赵

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值