并发包中ThreadLocalRandom类原理浅尝

  1. 云栖社区>
  2. 博客列表>
  3. 正文

并发包中ThreadLocalRandom类原理浅尝

加多 2018-06-04 11:07:58 浏览22008 评论3

摘要: # 一、 前言 ThreadLocalRandom类是JDK7在JUC包下新增的随机数生成器,它解决了Random类在多线程下多个线程竞争唯一的原子性种子变量的更新操作而导致大量线程自旋重试的不足。本节首先讲解下Random类的实现原理以及它在多线程下使用的局限性,然后引出ThreadLocalRandom类,通过讲解ThreadLocalRandom的实现原理来说明ThreadLocalRan

一、 前言

ThreadLocalRandom类是JDK7在JUC包下新增的随机数生成器,它解决了Random类在多线程下多个线程竞争唯一的原子性种子变量的更新操作而导致大量线程自旋重试的不足。本节首先讲解下Random类的实现原理以及它在多线程下使用的局限性,然后引出ThreadLocalRandom类,通过讲解ThreadLocalRandom的实现原理来说明ThreadLocalRandom是如何解决的Random类的不足。

二、 Random类原理及其局限性

在JDK7之前包括现在java.util.Random应该是使用比较广泛的随机数生成工具类,另外java.lang.Math中的随机数生成内部也是使用的java.util.Random的实例。下面先通过简单的代码看看java.util.Random是如何使用的:

public class RandomTest {
    public static void main(String[] args) {

        //(1)创建一个默认种子的随机数生成器
        Random random = new Random();
        //(2)输出10个在0-5(包含0,不包含5)之间的随机数
        for (int i = 0; i < 10; ++i) {
            System.out.println(random.nextInt(5));
        }
    }
}
  • 代码(1)创建一个默认随机数生成器,使用默认的种子。
  • 代码(2)输出输出10个在0-5(包含0,不包含5)之间的随机数。

这里提下随机数的生成需要一个默认的种子,这个种子其实是一个long类型的数字,这个种子要么在构造Random的实例的时候通过构造函数指定,那么默认构造函数内部会生成一个默认的值,有了默认的种子后,如何生成随机数那?

    public int nextInt(int bound) {
        //(3)参数检查
        if (bound <= 0)
            throw new IllegalArgumentException(BadBound);
        //(4)根据老的种子生成新的种子
        int r = next(31);
        //(5)根据新的种子计算随机数
        ...
        return r;
    } 

如上代码可知新的随机数的生成需要两个步骤

  • 首先需要根据老的种子计算生成新的种子。
  • 然后根据新的种子和bound变量通过一定的算法来计算新的随机数。

其中步骤(4)我们可以抽象为seed=f(seed),其中f是一个固定的函数,比如seed= f(seed)=a*seed+b;步骤(5)也可以抽象为g(seed,bound),其中g是一个固定的函数,比如g(seed,bound)=(int)((bound * (long)seed) >> 31);在单线程情况下每次调用nextInt(int)都是根据老的种子计算出来新的种子,这是可以保证随机数产生的随机性的。但是在多线程下多个线程可能都拿同一个老的种子去执行步骤(4)计算新的种子,这会导致多个线程产生的新种子是一样的,由于步骤(5)算法是固定的,所以会导致多个线程产生相同的随机值,这并不是我们想要的。所以步骤(4)要保证原子性,也就是说多个线程在根据同一个老种子计算新种子时候,第一个线程的新种子计算出来后,第二个线程要丢弃自己老的种子,要使用第一个线程的新种子来计算自己的新种子,依次类推,只有保证了这个,才能保证多线程下产生的随机数是随机的。Random类则通过使用一个原子变量种子达到了这个效果,在创建Random对象时候初始化的种子就保存到了种子原子变量里面,下面看下next()代码:

    protected int next(int bits) {
        long oldseed, nextseed;
        AtomicLong seed = this.seed;
        do {
            //(6)获取当前原子变量种子的值
            oldseed = seed.get();
            //(7)根据当前种子值计算新的种子
            nextseed = (oldseed * multiplier + addend) & mask;
            //(8)使用新种子替换老的种子
        } while (!seed.compareAndSet(oldseed, nextseed));
        //(9)
        return (int)(nextseed >>> (48 - bits));
    }
  • 代码(6)使用原子变量的get方法获取当前原子变量种子的值
  • 代码(7)根据具体的算法使用当前种子值计算新的种子
  • 代码(8)使用CAS操作,使用新的种子去更新老的种子,多线程下可能多个线程都同时执行到了代码(6)那么可能多个线程都拿到的当前种子的值是同一个,然后执行步骤(7)计算的新种子也都是一样的,但是步骤(8)的CAS操作会保证只有一个线程可以更新老的种子为新的,失败的线程会通过循环从新获取更新后的种子作为当前种子去计算老的种子,可见这里解决了上面提到的问题,也就保证了随机数的随机性。
  • 代码(9)则使用固定算法根据新的种子计算随机数,并返回。

这里总结下Random类生成随机数原理以及不足:每个Random实例里面有一个原子性的种子变量用来记录当前的种子的值,当要生成新的随机数时候要根据当前种子计算新的种子并更新回原子变量。

多线程下使用单个Random实例生成随机数时候,多个线程同时计算随机数计算新的种子时候多个线程会竞争同一个原子变量的更新操作,由于原子变量的更新是CAS操作,同时只有一个线程会成功,那么CAS操作失败的大量线程进行自旋重试,而大量线程的自旋重试是会降低并发性能和消耗CPU资源的,为了解决这个问题,ThreadLocalRandom类应运而生。

三、 ThreadLocalRandom类

3.1、 ThreadLocalRandom的简单使用与原理

为了解决多线程高并发下Random类生成随机数的的缺陷,JUC包下新增了ThreadLocalRandom类,下面首先通过简单的代码看看看它如何使用:

public class RandomTest {

    public static void main(String[] args) {
        //(10)获取一个随机数生成器
        ThreadLocalRandom random =  ThreadLocalRandom.current();
        
        //(11)输出10个在0-5(包含0,不包含5)之间的随机数
        for (int i = 0; i < 10; ++i) {
            System.out.println(random.nextInt(5));
        }
    }
}

如上代码(10)调用ThreadLocalRandom.current()来获取当前线程的随机数生成器。下面来分析下ThreadLocalRandom的实现原理。从名字看会让我们联想到基础篇讲解的ThreadLocal类。ThreadLocal的出现就是为了解决多线程访问一个变量时候需要进行同步的问题。ThreadLocal通过让每一个线程拷贝一份变量,每个线程对变量进行操作时候实际是操作自己本地内存里面的拷贝,从而避免了对共享变量进行同步。实际上ThreadLocalRandom的实现也是这个原理。Random的缺点是多个线程会使用原子性种子变量,会导致对原子变量更新的竞争,这个原理可以通过下面图来表达:
image.png

那么如果每个线程维护自己的一个种子变量,每个线程生成随机数时候根据自己本地内存中的老的种子计算新的种子,并使用新种子更新老的种子,然后根据新种子计算随机数,就不会存在竞争问题,这会大大提高并发性能,如下图ThreadLocalRandom原理可以使用下图表达:
image.png

3.2、 ThreadLocalRandom源码分析

下面首先通过ThreadLocalRandom的类图结构来对ThreadLocalRandom类有个全局的认识:
image.png

如上类图可知ThreadLocalRandom类继承了Random类并重写了nextInt(int)方法,其实ThreadLocalRandom中并没有使用继承自Random的原子性种子变量。并且ThreadLocalRandom中也并没有具体存放种子,具体的种子是存放到调用线程的threadLocalRandomSeed变量里面的,ThreadLocalRandom类似于ThreadLocal类就是个工具类。当线程调用ThreadLocalRandom的current()方法时候ThreadLocalRandom负责初始化调用线程的threadLocalRandomSeed变量,也就是初始化种子。当调用ThreadLocalRandom的nextInt(int)方法时候,实际上是获取当前线程的threadLocalRandomSeed变量作为当前种子来计算新的种子,然后更新新的种子到当前线程的threadLocalRandomSeed变量,然后在根据新种子和具体算法计算随机数。这里需要注意的是threadLocalRandomSeed变量就是Thread类里面的一个普通long变量,并不是原子性变量,其实道理很简单,因为这个变量是线程级别的,根本不需要使用原子性变量,如果还是不理解可以思考下ThreadLocal的原理。

其中变量seeder和probeGenerator是两个原子性变量,在初始化调用线程的种子和探针变量时候用到,每个线程只会使用一次。
其中seeder的定义如下:

 private static final AtomicLong seeder = new      AtomicLong(initialSeed());
private static long initialSeed() {

        //获取变量java.util.secureRandomSeed的值
        String pp = java.security.AccessController.doPrivileged(
                new sun.security.action.GetPropertyAction(
                        "java.util.secureRandomSeed"));
        //如果变量值为true则使用安全性较高的种子,这个种子是不可被预测的
        if (pp != null && pp.equalsIgnoreCase("true")) {
            byte[] seedBytes = java.security.SecureRandom.getSeed(8);
            long s = (long)(seedBytes[0]) & 0xffL;
            for (int i = 1; i < 8; ++i)
                s = (s << 8) | ((long)(seedBytes[i]) & 0xffL);
            return s;
        }
        //否者使用当前时间来计算初始化种子
        return (mix64(System.currentTimeMillis()) ^
                mix64(System.nanoTime()));
    }

如上代码种子seeder是一个原子性变量,使用initialSeed计算其初始化值,代码内首先判断 java.util.secureRandomSeed 是否为true来判断要不要使用安全性高的种子,如果为true则使用SecureRandom.getSeed(8)获取初始化种子,这里说的安全是指获取的种子是不可被预测的。如果不需要安全性高的种子则使用当前时间来计算初始化种子,这个种子是可以被预测到的。
其中probeGenerator的定义如下:

private static final AtomicInteger probeGenerator =
       new AtomicInteger();

可知probeGenerator的初始化值为0;

另外变量instance是个ThreadLocalRandom的一个实例,该变量是static的,当多线程通过ThreadLocalRandom的current方法获取ThreadLocalRandom的实例时候其实获取的是同一个,但是由于具体的种子是存放到线程里面的,所以ThreadLocalRandom的实例里面只是与线程无关的通用算法,所以是线程安全的。

下面看看ThreadLocalRandom的主要代码实现逻辑

  • Unsafe 机制的使用
    private static final sun.misc.Unsafe UNSAFE;
    private static final long SEED;
    private static final long PROBE;
    private static final long SECONDARY;
    static {
        try {
            //获取unsafe实例
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class<?> tk = Thread.class;
            //获取Thread类里面threadLocalRandomSeed变量在Thread实例里面偏移量
            SEED = UNSAFE.objectFieldOffset
                (tk.getDeclaredField("threadLocalRandomSeed"));
            //获取Thread类里面threadLocalRandomProbe变量在Thread实例里面偏移量
            PROBE = UNSAFE.objectFieldOffset
                (tk.getDeclaredField("threadLocalRandomProbe"));
            //获取Thread类里面threadLocalRandomProbe变量在Thread实例里面偏移量
            SECONDARY = UNSAFE.objectFieldOffset
                (tk.getDeclaredField("threadLocalRandomSecondarySeed"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }

这里代码很简单,首先获取了unsafe的实例,然后获取Thread类里面的threadLocalRandomSeed和threadLocalRandomProbe变量在Thread内存结构里面的位置。

  • ThreadLocalRandom current()方法:该方法获取ThreadLocalRandom实例,并初始化调用线程中threadLocalRandomSeed和threadLocalRandomProbe变量。
    static final ThreadLocalRandom instance = new ThreadLocalRandom();
    public static ThreadLocalRandom current() {
        //(12)如果当前线程threadLocalRandomProbe为0则进行首次初始化
        if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
            //(13)具体对threadLocalRandomProbe进行初始化
            localInit();
        //(14)返回单例实例
        return instance;
    }
    static final void localInit() {
        //原子性操作递增PROBE_INCREMENT,获取当前线程应该分配的基础探针值
        int p = probeGenerator.addAndGet(PROBE_INCREMENT);
        int probe = (p == 0) ? 1 : p; // skip 0
        //原子性操作递增SEEDER_INCREMENT,获取当前线程应该分配的初始化种子
        long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));
        //设置当前线程的变量threadLocalRandomProbe和threadLocalRandomSeed的值
        Thread t = Thread.currentThread();
        UNSAFE.putLong(t, SEED, seed);
        UNSAFE.putInt(t, PROBE, probe);
    }

如上代码(12)如果当前线程中threadLocalRandomProbe变量值为0(默认情况下线程的这个变量为0),说明当前线程第一次调用ThreadLocalRandom的current()方法,那么就需要调用localInit()方法计算当前线程的初始化种子变量。这里设计为了延迟初始化,不需要使用随机数功能时候Thread类中的种子变量就不需要被初始化,这是一种优化。

代码(13)首先计算根据probeGenerator计算当前线程中threadLocalRandomProbe的初始化值,然后根据seeder计算当前线程的初始化种子threadLocalRandomSeed,然后把这两个变量设置到当前线程。
代码(14)返回ThreadLocalRandom的实例,需要注意的是这个方法是静态方法,多个线程返回的是同一个ThreadLocalRandom实例。
另外看下ThreadLocalRandom的默认构造函数如下:

    private ThreadLocalRandom() {
        initialized = true; 
    }

可知构造函数为private修饰,意味着不能使用new操作构造新的实例,只有通过`
ThreadLocalRandom.current()`来获取ThreadLocalRandom单例实例。

  • void setSeed(long seed) 设置计算随机数的种子的值
    public void setSeed(long seed) {
    
        if (initialized)
            throw new UnsupportedOperationException();
    }

可知ThreadLocalRandom并没有对外提供设置初始化种子的机制,这个方法只有在它的父
类的构造函数里面才会被调用,如下代码:

    public Random() {
        this(seedUniquifier() ^ System.nanoTime());
    }
   public Random(long seed) {
   if (getClass() == Random.class)
       this.seed = new AtomicLong(initialScramble(seed));
   else {
       // 这里调用的是ThreadLocalRandom的setSeed方法
       this.seed = new AtomicLong();
       setSeed(seed);
   }
 }

当调用ThreadLocalRandom的默认构造函数创建ThreadLocalRandom实例时候,ThreadLocalRandom的构造函数内部会先去调用Random的无参构造函数,而后者会调用Random(long seed),这个函数里面会调用ThreadLocalRandom类实现的setSeed方法,由于这时候initialized变量还是为false,所以并没有抛出异常,等Random(long seed)方法执行完毕也就是ThreadLocalRandom实例里面的Random构造完毕后,才会去执行ThreadLocalRandom的构造函数里面的代码,也就是设置initialized为true。

  • int nextInt(int bound)方法:计算当前线程的下一个随机数
    public int nextInt(int bound) {
        //(15)参数校验
        if (bound <= 0)
            throw new IllegalArgumentException(BadBound);
        //(16) 根据当前线程中种子计算新种子
        int r = mix32(nextSeed());
        //(17)根据新种子和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;
    }

如上代码逻辑步骤与Random相似,也是先对bound参数进行校验,然后根据当前线程的当前种子值计算新种子,然后根据新种子和bound的值计算随机数并返回,下面我们重点看下nextSeed()方法:

    final long nextSeed() {
        Thread t; long r; // 
        UNSAFE.putLong(t = Thread.currentThread(), SEED,
                       r = UNSAFE.getLong(t, SEED) + GAMMA);
        return r;
    }

如上代码首先使用 r = UNSAFE.getLong(t, SEED)获取当前线程中threadLocalRandomSeed变量的值,然后在种子的基础上累加GAMMA值作为新种子,然后使用UNSAFE的putLong方法把新种子放入当前线程的threadLocalRandomSeed变量。

四、 总结

本节首先讲解了Random类的实现原理以及介绍了Random类在多线程下存在竞争种子原子变量更新操作失败后自旋等待的缺点,从而引出ThreadLocalRandom类,ThreadLocalRandom使用ThreadLocal的原理,让每个线程内持有一个本地的种子变量,该种子变量只有在使用随机数时候才会被初始化,多线程下计算新种子时候是根据自己线程内维护的种子变量进行更新,从而避免了竞争。所以在项目实践中如果需要在多个线程下使用单个随机数生成器实例生成随机数则ThreadLocalRandom是很好的选择,如果你只需要在单线程里面使用一个随机数生成器的实例则选择Random类是很好的选择。

【云栖快讯】你想见的Java技术专家都在这了,向大佬提问,有问题必答  详情请点击

网友评论

2F
gydtep

申请阿里云服务器等产品时,可以使用1000元阿里云代金券,领取网址: https://promotion.aliyun.com/ntms/yunparter/invite.html?userCode=2a7uv47d

加多
文章18篇 | 关注19
关注
用于实时预测用户对物品偏好,支持企业定制推荐算法,支持A/B Test效果对比 查看详情
大数据开发套件(Data IDE),提供可视化开发界面、离线任务调度运维、快速数据集成、多人... 查看详情
为您提供简单高效、处理能力可弹性伸缩的计算服务,帮助您快速构建更稳定、安全的应用,提升运维效... 查看详情
阿里云总监课正式启航

阿里云总监课正式启航