Random和ThreadLocalRandom

在日常项目开发中,随机的场景需求经常发生,如红包、负载均衡等等。在 Java 中的,使用随机,一般使用 Random 或者 Math.random()。这篇文章中主要就来介绍下 Random,以及在并发环境下一些更好的选择 ThreadLocalRandom。

一. Random

1.Random 使用

Random 类位于 java.util 包下,是一种伪随机。它主要提供了一下几种不同类型的随机数接口:

  • nextBoolean(),boolean 类型的随机,随机返回 true/false

  • nextFloat(),float 类型的随机,随机返回 0.0 - 1.0 之间的 float 类型

  • nextDouble(),double 类型的随机,随机返回 0.0 - 1.0 之间的 double 类型

  • nextLong(),long 类型的随机,随机返回 long 类型的随机数

  • nextInt(),int 类型的随机,随机返回 int 类型的随机数

  • nextInt(int bound),同 nextInt(),但是返回的 int 上界是 bound,且不包括 bound,下界是 0

  • nextGaussian(),double 类型的随机,随机返回 0.0 - 1.0 之前的 double 类型,但是它整体会表现出高斯分布

Random 中,十分关键的是 Seed,它是一个 48bit 的的随机种子。换而言之,Random 的随机是一种伪随机,它也是一套非常复杂的算法,生成的随机数也是有规律可循。这套算法的执行需要一个初始数值,在 Random 中初始值,就是 seed 随机种子。对于相同的 Seed,调用随机方法相同,得到的随机数是一样的。

接下来,看一个使用 Random 实现随机红包的功能。输入两个参数,第一个是红包总金额,第二个是红包个数:

    /**
     * 随机红包实现
     *
     * @param totalAmount 红包总金额
     * @param nums 红包个数
     * @author huaijin
     */
    static List<Long> randomRedEnvelope(Long totalAmount, int nums) {
        if (nums == 0) {
            throw new RuntimeException("红包个数需要大于0.");
        }
        List<Long> redEnvelope = new ArrayList<>(nums);
        Random random = new Random();
        long remaining = totalAmount;
        for (int i = 0; i < nums - 1; i++) {
            double probability = random.nextDouble();
            Long subAmount = Math.round(remaining * probability);
            if (subAmount == 0 || remaining - subAmount == 0) {
                continue;
            }
            redEnvelope.add(subAmount);
            remaining = remaining - subAmount;
        }
        redEnvelope.add(remaining);
        return redEnvelope;
    }

这里利用 Random 的 nextDouble() 的特性,每次生成 0.0 - 1.0 之间的数字,作为红包的占比,以达到随机红包的实现。

2.Random 线程安全保证

Random 在多线程的环境是并发安全的,它解决竞争的方式是使用用原子类,本质上上也就是 CAS + Volatile 保证线程安全。接下来就分析下其原理,以理解线程安全的实现。

在 Random 类中,有一个 AtomicLong 的域,用来保存随机种子。其中每次生成随机数时都会根据随机种子做移位操作以得到随机数。如 Long 类型的随机:

long 类型在 Java 中总弄 64bit,对 next 方法的返回值左移 32 作为 long 的高位,然后将 next 方法返回值作为低 32 位,作为 long 类型的随机数。此处关键之处在于 next 方法,以下是 next 方法的核心

使用 seed 种子,不断生成新的种子,然后使用 CAS 将其更新,再返回种子的移位后值。这里不断的循环 CAS 操作种子,直到成功。

可见,Random 实现原理主要是利用随机种子采用一定算法进行处理生成随机数,在随机种子的安全保证利用原子类 AtomicLong。

3. 并发下 Random 的不足

以上分析了 Random 的实现原理,虽然 Random 是线程安全,但是对于并发处理使用原子类 AtomicLong 在大量竞争时,由于很多 CAS 操作会造成失败,不断的 Spin,而造成 CPU 开销比较大而且吞吐量也会下降。

现在发现问题就是大量的并发竞争,使得 CAS 失败,对于竞争问题的优化策略在前文 AtomicLong 和 LongAdder 时,也谈到了。锁的极限优化是 Free Lock,如 ThreadLoal 方式。

在 JDK 1.7 中由并发大神引入了 ThreadLocalRandom 来解决 Random 的大并发问题,以下两者的测试结果比较。每个线程生成 10w 次,运行 12 次,去掉了最大最小值的平均结果:

可以看出 Random 随着竞争越来越激烈,然后耗时越来越多,说明吞吐量将成倍下降。然而 ThreadLocalRandom 随着线程数的增加,基本没有变化。所以在大并发的情况下,随机的选择,可以考虑 ThreadLocalRandom 提升性能,也是性能优化之道的一步。

二. 更好的选择 ThreadLocalRandom

ThreadLocalRandom 是 Random 的子类,它是将 Seed 随机种子隔离到当前线程的随机数生成器,从而解决了 Random 在 Seed 上竞争的问题,它的处理思想和 ThreadLocal 本质相同。这里开门见山,直接看源码,分析其实现原理。

使用 ThreadLocalRandom 的方式为

ThreadLocalRandom.current().nextX(...)

其中 X 表示,Int、Long、Double、Float、Boolean 等等。按照这样的方法调用逐步深入其中细节:

从上述代码中显而意见就可以看出使用了单例模式,当 UNSAFE.getInt(Thread.currentThread(), PROBE) 返回 0 时,就执行 localInit(),否则就返回单例。

首先看单例 instance

从注释中也可以看出,是一个公共的 ThreadLocalRandom,也就是说,在一个 Java 应用中只有一个 ThreadLocalRandom 对象,显然是单例,即无论哪个线程执行随机时都是使用这个单例对象。

那么单例情况下又如何将随机种子隔离呢?

再来看下 UNSAFE.getInt(Thread.currentThread(), PROBE),这条语句主要是获取当前 Thread 对象中的 PROBE,再看看 PROBE 的初始化

PROBE 是 Thread 中 threadLocalRandomProbe

从注释中可以看出,threadLocalRandomProbe 用于表示 ThreadLocalRandom 是否初始化,如果是非 0,表示其已经初始化。换句话说,该变量就是状态变量,用于标识 ThreadLocalRandom 是否被初始化。

其中还有个非常关键的 threadLocalRandomSeed,从注释中也可以看出,它是当前线程的随机种子。到这里,一下子豁然开朗,随机种子分散在各个 Thread 对象中,从而避免了并发时的竞争点。

那么它又是什么时候初始化的呢?

当 Thread 对象被创建后,threadLocalRandomProbe 和 threadLocalRandomSeed 应该都是 0。当在这个线程中首次调用 ThreadLocalRandom.current 时,threadLocalRandomProbe 为 0,会执行 localInit。其中会初始化 threadLocalRandomSeed,并将 threadLocalRandomProbe 更新为非 0,表示已经初始化。

上面核心的两步骤,初始化 Thread 中的 threadLocalRandomSeed 和 threadLocalRandomProbe。

当 localInit 执行后,就返回 ThreadLocalRandom 的单例供应用使用 nextX() 系列方法生成随机数。再来看下 nextInt 的实现

从以上可以看出,当生成 int 随机数时,每次都利用 Unsafe 工具获取当前 Thread 对象中的随机种子生成随机数。并且每次获取的时候,都将 Seed 种子增加 GAMMA,以供下次使用。

三. 总结

Random 是 Java 中提供的随机数生成器工具类,但是在大并发的情况下由于其随机种子的竞争会导致吞吐量下降,从而引入 ThreadLocalRandom。它将竞争点隔离到每个线程中,从而消除了大并发情况下竞争问题,提升了性能。

从两者的设计上,可以看出在处理并发优化时的优秀设计思想,对于竞争问题,可以将将竞争点隔离,如使用 ThreadLocal 实现。

并发竞争的整体优化思路,还是像前文中总结的一样:

lock -> cas + volatile -> free lock

只会 free lock 的设计方式就是避免竞争,将竞争点隔离到线程中,从而解决竞争。


作者:怀瑾握瑜

来源链接:

https://www.cnblogs.com/lxyit/p/12654374.html

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值