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的设计方式就是避免竞争,将竞争点隔离到线程中,从而解决竞争。

posted @ 2020-04-07 16:58  怀瑾握瑜XI  阅读(1822)  评论(1编辑  收藏  举报