Java并发包–ThreadlocalRandom原理解密

Java
309
0
0
2023-06-09
标签   Java并发

​ThreadLocalRandom是JDK1.7新增的随机生成器,我们今天来说明为什么要JUC下要新增这个类,以及解密它的原理

首先我们看看我们熟悉的Random类,我们现提出两个问题,跟着问题看文章,可能事半功倍哟

  1. 它的实现原理
  2. 它有什么缺点

随机数需要生成一个默认的种子,这个种子其实就是一个long类型的数字,你可以在创建Random对象时候通过构造函数指定,如果不指定则会默认在构造函数中生成一个默认的值,有了默认的种子,他是如何生成随机数呢?

 public int nextInt(int bound) {        //.参数校验
        if (bound <= )
            throw new IllegalArgumentException(BadBound);
        //.根据老的种子生成新的种子
        int r = next();        //3.根据新的种子计算随机数
        int m = bound - ;
        if ((bound & m) == )  // i.e., bound is a power of 2
            r = (int)((bound * (long)r) >> );
        else {
            for (int u = r;
                 u - (r = u % bound) + m < ;
                 u = next())
                ;
        }
        return r;
}  

重要步骤只要两点

  1. 根据老的种子生成新的种子
  2. 根据新的种子生成随机数

在单 线程 中,nextInt都是根据老种子生成新的种子,这是可保证随机数产生的随机性,但是在多线程中多个线程可能都拿到同一个老种子计算新的种子,由于新的种子计算随机数是固定的函数,因此多个线程可能根据同样的种子生成相同的随机数,当然这并不是我期望的,因此Ran do m要保证2步骤的原子性.也就是说当多个线程同时获取到同一个新的 种子,只有一个线程可以根据新种子计算出随机数,其他线程都会丢弃掉拿到的种子。这样才能保证每一个线程获取的随机数都是随机的。

 protected int next(int bits) {
        long oldseed, nextseed;
        AtomicLong seed = this.seed;
        do {             // 获取当前原子变量的种子
            oldseed = seed.get();             //根据老的种子生成新的种子
            nextseed = (oldseed * multiplier + addend) & mask;             //使用CAS操作
        } while (!seed.compareAndSet(oldseed, nextseed));
        return (int)(nextseed >>> ( - bits));
    }  

第6步骤是最要的,他是使用CAS操作。当多个线程获取到相同的种子,在第6步骤的时候可以保证只有一个线程更新老的种子为新的,其他线程会继续循环去重新获取种子,这样就保证随机数的随机性。

但是,有使用CAS操作,会导致多个线程的进行自旋重试,这就会降低并发性能,所以 ThreadLocal Random应运而生。

ThreadLocalRandom的实现原理

ThreadLocalRandom我们可以联想ThreadLocal,ThreadLocal的实现原理是通过每个线程复制一个变量,使得每个线程对变量进行操作时实际上是操作自己本地内存里面的副本,从而避免了对共享变量进行同步,实际上ThreadLocalRandom也是这个实现原理,Random的缺点就是多个线程使用同一个原子种子变量,从而导致了原子变量的竞争,如下图

Java并发包--ThreadlocalRandom原理解密

那么,如果每个线程都维护一个种子变量,则每个线程都会根据自己的 种子生成新的种子,再根据新的种子计算出随机数,这样就避免竞争问题,这就会大大提高并发性能,实现原理如下,

Java并发包--ThreadlocalRandom原理解密

源码分析

Java并发包--ThreadlocalRandom原理解密

可以看出ThreadLocalRandom类继承了Random,并重写了nextint方法,在ThreadlocalRandom没有继承自Random的原子种子变量,在ThreadLocalRandom并没有具体的种子,具体的种子存放到具体调用线程的threadlocalrandomSeed变量里面,就和ThreadLocal一样就是一个工具类,当线程调用ThreadlocalRandom的current,ThreadLocalRandom负责初始化threalocalrandomseed变量,就是初始化种子

当调用ThreadLocalRandom的nextInt的时候,实际上就是获取当前线程的threadlcoalrandomseed变量作为种子来计算新的种子,然后更新threadlocalrandomseed变量,根据新的种子使用具体的算法计算随机数,需要注意的是,threadlocalrandomseed仅仅是一个普通的long类型变量,并不是原子类型,但是由于他是线程级别的,所以他就不会存在 线程安全 的问题,并需要原子性变量,

其中seed和probeGenerator是原子变量,他是在初始化种子的时候使用,每个线程只会调用一次,另外变量instance是ThreadLocalRandom的一个实例,该变量是 static ,多个线程使用的实例是同一个,但是由于具体的种子存在在线程里面的,所以在ThreadlocalRandom的实例里面只包含线程无关的的通用算法,因此他是线程安全的。

  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);
        }
    }  

ThreadLocalRandom的current方法

该方法获取到ThreadlocalRandom的实例,并且初始化调用线程的thredLocalRandomSeed和threadlocalRandomProbe变量。

 static final ThreadLocalRandom instance = new ThreadLocalRandom();
    public static ThreadLocalRandom current() {
        //()
        if (UNSAFE.getInt(Thread.currentThread(), PROBE) == )
            //()
            localInit();
        //()
        return instance;
    }

static final void localInit() {
        int p = probeGenerator.addAndGet(PROBE_INCREMENT);
        int probe = (p == ) ? 1 : p; // skip 0
        long seed = mix(seeder.getAndAdd(SEEDER_INCREMENT));
        Thread t = Thread.currentThread();
        UNSAFE.putLong(t, SEED, seed);
        UNSAFE.putInt(t, PROBE, probe);
    }  

如上面代码(7),如果线程的threadlocalRandomProbe的变量值为0时(默认他的值就是0),则说明他是第一次调用current,那么就会使用localinit()方法计算当前线程的初始化种子,这里是为了延迟初始化,在不使用生成随机数功能时,就不会初始化线程的种子变量,这也是一种优化,

代码(8)首先根据probeGenerator计算当前线程中ThreadLocalRandomProbe的初始值,然后根据seeder计算当前线程的初始化种子,而后把这两个变量放到当前线程,代码9返回ThreadLocalRandom的实例,需要注意的是这个方法是静态方法,多个线程返回的是同一个实例,

nextInt(int bound)

 public int nextInt(int bound) {
        //()参数校验
        if (bound <= )
            throw new IllegalArgumentException(BadBound);
        //() 根据当前线程中种子计算新种子
        int r = mix(nextSeed());
        //()根据新种子和bound计算随机数
        int m = bound - ;
        if ((bound & m) == ) // power of two
            r &= m;
        else { // reject over-represented candidates
            for (int u = r >>> ;
                 u + m - (r = u % bound) < ;
                 u = mix(nextSeed()) >>> 1)
                ;
        }
        return r;
    }  

如上代码的逻辑步骤和random相似,重点看一下,

 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,SEND)获得当前threadlocalrandomSeed的值,然后在种子的基础上加上GAMMA作为新种子,在使用UNSAFE的putLong方法把新的种子放入到当前线程的threadLocalRandomSeed变量中。

由于Random的缺点,从而引出了ThreadlcoalRandom类,ThreadLocalRandom使用Threadlocall的原理,让每个线程都持有一个本地的种子变量,该种子变量只有在使用随机数时才会被初始化,在多个线程中每个线程根据自己的种子进行更新,从而避免了并发竞争。

喜欢这篇文章的人也喜欢 · · · · · ·

▶ 面试Threadlocal源码解析

希望此文对大家有所帮助,也希望大家持续关注转载。关注公众号获取相关资料请回复:typescript,springcloud,springboot,nodejs,nginx,mq, java web,java并发实战,java并发高级进阶,实战java并发,极客时间dubbo,kafka,java面试题,ES,zookeeper,java入门到精通,区块链,java优质视频,大数据,kotlin,瞬间之美,HTML与CSS,深入体验java开发,web开发CSS系列,javaweb开发详解,springmvc,java并发编程,spring源码,python,go,redis,docker,即获取相关资料。