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

  • 相关阅读:
    多表模型
    母版,单表操作,双下划线模糊查询
    模板层
    视图层
    路由层
    orm
    浅谈cookie,sessionStorage和localStorage区别
    实现元素垂直居中的方法(补充)
    实现元素垂直居中的方法
    <a href="javascript:;"></a>
  • 原文地址:https://www.cnblogs.com/lxyit/p/12654374.html
Copyright © 2011-2022 走看看