xiaobenchi

导航

Java并发包中ThreadLocalRandom类原理剖析

Java并发包中ThreadLocalRandom类原理剖析

ThreadLocalRandom类是JDK 7在JUC包下新增的随机数生成器。

1. Random类及其局限性

  • java.util.Random的使用方法

    public class RandomTest{
        public static void main(String[] args){
            
            //1. 创建一个默认种子的随机数生成器
            Random random = new Random();
            //2. 输出10个在0~5之间的随机数
            for(int i = 0; i < 10; ++i){
                System.out.println(random.nextInt(5));
            }
        }
    }
    
  • 随机数生成

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

由此可见,新的随机数的生成需要两个步骤:

1、 首先根据老的种子生成新的种子。

2、然后根据新的种子计算随机数

在多线程的情况下,可能会有多个新的线程拿了同一个老的种子去计算新的种子,这样会导致多个线程产生相同的随机值。所以步骤4要保证原子性,也就是说当多个线程根据同一个老种子计算新种子时,第一个线程的新种子被计算出来后,第二个线程要丢弃自己老的种子,而使用第一个线程的新种子来计算自己的新种子,依此类推,只有保证了这个,才能保证在多线程下产生的随机数是随机的。

每个Random实例里面都有一个原子性的种子变量用来记录当前的种子值,当要生成新的随机数时需要根据当前种子计算新的种子并更新回原子变量。在多线程下使用单个Random实例生成随机数时,当多个线程同时计算随机数来计算新的种子时,多个线程会竞争同一个原子变量的更新操作,由于原子变量的更新是CAS操作,同时只有一个线程会成功,所以会造成大量线程进行自旋重试,这会降低并发性能,所以ThreadLocalRandom应运而生。

2. ThreadLocalRandom

为了弥补多线程高并发情况下Random的缺陷,在JUC包下新增了ThreadLocalRandom类。

//ThreadLocalRandom的使用
public class RandomTest{
    
    public static void mian(String[] args){
        //1. 获取一个随机数生成器
        ThreadLocalRandom random = ThreadLoaclRandom.current();
        //2. 输出10个在0~5之间的随机数
        for(int i = 0; i < 10; ++i){
            Sysyem.out.println(random.nextInt(5));
        }
    }
}

Random的缺点是多个线程会使用同一个原子性种子变量,从而导致对原子变量更新的竞争那么,如果每个线程都维护一个种子变量,则每个线程生成随机数时都根据自己老的种子计算新的种子,并使用新种子更新老的种子,再根据新种子计算随机数,就不会存在竞争问题了,这会大大提高并发性能。

image-20220727154530962

3. 源码分析

  • ThreadLocalRandom的类图结构

    image-20220727154630492

从图中可知,ThreadLocalRandom类继承了Random类并重写了nextInt方法,在ThreadLocalRandom中并没有使用继承自Random类的原子性种子变量。在ThreadLocalRandom中并没有存放具体的种子,具体的种子存放在具体的调用线程的threadLocalRandomSeed变量里面。ThreadLocalRandom类似于ThreadLocal类,就是个工具类。当线程调用ThreadLocalRandom的current方法时,ThreadLocalRandom负责初始化调用线程的threadLocalRandomSeed变量,也就是初始化种子。

  • ThreadLocalRandom的主要代码的实现逻辑

    1. Unsafe机制

      Unsafe机制

  1. ThreadLoaclRandom current()方法

    该方法获取ThreadLocalRandom()实例,并初始化调用线程中的threadLocalRandomSeedthreadLoaclRandomProbe变量。

    static final ThreadLoaclRandom instance = new ThreadLocalRandom();
    public static ThreadLoaclRandom current(){
        if(UNSAFE.getInt(Thread.currentThread(),PROBE) == 0)
            localInit();
        return instance;
    }
    
    static fianl void localInit(){
        int p = probeGenerator.addAndGet(PROBE_INCREMENT);
        int probe = (p == 0)?1:p;
        long seed = mix64(seeder.getAndAdd(SEEDER_INCREAMENT));
        thread t = Thread.currentThread();
        UNSAFE.putLong(t,SEED,seed);
        UNSAFE.putInt(t,PROBE,probe);
    }
    
  2. int nextInt(int bound)方法

    //计算当前线程的下一个随机数
    public int nextInt(int bound){
        if(bound <= 0){
            throw new IllegalArgumentException(BadBound);
        }
        
        int r = mix32(nextSeed());
        int m = bound - 1;
        if((bound&m) == 0){
            r &= m;
        }else{
            for(int u = r >>>1;u + m - (r = u % bound) < 0;
               u = mix32(nextSeed()) >>> 1)
                return r;
        }
    }
    

    看一下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变量中。

posted on 2022-07-27 16:23  小迟在努力  阅读(56)  评论(0编辑  收藏  举报