并发包中ThreadLocalRandom作用-CAS是什么?为什么使用AtomicLong而不是Long?

ThreadLocalRandom类——>为什么使用它?

ThreadLocalRandom类是随机数生成器,它解决了Random类在多线程下多个线程竞争内部唯一的原子性种子变量,导致大量线程自旋重试的不足(文下有解释)。本节首先讲解下Random类的实现原理已经它在多线程下使用的局限性,然后引入ThreadLocalRandom 类,通过讲解ThreadLocalRandom的实现原理来说明ThreadLocalRandom是如何解决的Random类的不足。

————本文为精简内容,引入wangyunpeng0319的博客

Random类及其局限性

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

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

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

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

  • 首先需要根据老的种子生成新的种子。
  • 然后根据新的种子来计算新的随机数

 在单线程情况下每次调用nextInt都是根据老的种子计算出来新的种子,这是可以保证随机数产生的随机性的。但是在多线程下多个线程可能都拿同一个老的 种子去执行步骤(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));
    }
//什么是原子变量? 什么是cas? 为什么要使用atomiclong 而不是 long? 首先要明白这个
//
//首先你得了解synchronized(同步锁),atomiclong 可以理解是加了synchronized的long。
//
//现在我们思考一个java场景,跟多线程有关,而且可能用于编写计数器。
//
//了解JVM指令的,都知道如果赋值一个long变量,编译后,会产生多个JVM语言指令,如果处于多线程情况下对于这个变量进行加减操作会导致数据不一致。
//
//为避免此问题, Java 引入了原子变量 AtomicLong 。当一个线程正在操作一个原子变量时,即使其他线程也想要操作这个变量,
//类的实现中含有一个检查那步骤操作是否完成的机制。 基本上,操作获取变量的值,改变本地变量值,然后尝试以新值代替旧值。
//如果旧值还是一样,那么就改变它。如果不一样,方法再次开始操作。这个操作称为  Compare and Set (校对注:简称 CAS ,比较并交换的意思)。
  • 代码(6)获取当前原子变量种子的值
  • 代码(7)根据当前种子值计算新的种子
  • 代码(8)使用CAS操 作,使用新的种子去更新老的种子,多线程下可能多个线程都同时执行到了代码(6)那么可能多个线程都拿到的当前种子的值是同一个,然后执行步骤(7)计算 的新种子也都是一样的,但是步骤(8)的CAS操作会保证只有一个线程可以更新老的种子为新的,失败的线程会通过循环从新获取更新后的种子作为当前种子去 计算老的种子,这里解决了上面提到的问题,也就保证了随机数的随机性。
  • 代码(9)则使用固定算法根据新的种子计算随机数。

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

ThreadLocalRandom类

为解决多线程高并发下Random的缺陷,新增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));
        }
    }
}

分析下ThreadLocalRandom的实现原理。类似ThreadLocal类,ThreadLocal的出现就是为了解决多线程访问一个变量时候需要进行同步的问题,让每一个线程拷贝一份变量, 每个线程对变量进行操作时候实际是操作自己本地内存里面的拷贝,从而避免了对共享变量进行同步。实际上ThreadLocalRandom的实现也是这个 原理,Random的缺点是多个线程会使用原子性种子变量,会导致对原子变量更新的竞争(若看到段有疑问的请不要停下来自己想,看下段)

那么如果每个线程维护自己的一个种子变量,每个线程生成随机数时候根据自己老的种子计算新的种子,并使用新种子更新老的种子,然后根据新种子计算随机数,就不会存在竞争问题,这会大大提高并发性能。

ThreadLocalRandom类结构

以下>>>ThreadLocalRandom = TLR

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

其中变量seeder和probeGenerator是两个原子性变量,在初始化调用线程的种子和探针变量*时候用到,每个线程只会使用一次。变量instance是TLR的一个实例,该变量是static的,当多线程通过TLR的 current方法获取TLR的实例时候其实获取的是同一个,但由于具体的种子是存放到线程里面的,所以TLR的实例里面 只是一些与线程无关的通用算法,所以是线程安全的。

ThreadLocalRandom的主要代码

  • Unsafe 类的使用——>>建议看注释即可--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);
        }
    }
  •  ThreadLocalRandom current()方法:该方法获取TLR实例,并初始化调用线程中threadLocalRandomSeed和threadLocalRandomProbe变量。
        static final ThreadLocalRandom instance = new ThreadLocalRandom();
        public static ThreadLocalRandom current() {
            //(12)
            if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
                //(13)
                localInit();
            //(14)
            return instance;
        }
        static final void localInit() {
            int p = probeGenerator.addAndGet(PROBE_INCREMENT);
            int probe = (p == 0) ? 1 : p; // skip 0
            long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));
            Thread t = Thread.currentThread();
            UNSAFE.putLong(t, SEED, seed);
            UNSAFE.putInt(t, PROBE, probe);
        }

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

    代码(13)首先计算根据probeGenerator计算当前线程中threadLocalRandomProbe的初始化值,然后根据seeder计算当前线程的初始化种子,然后把这两个变量设置到当前线程。
    代码(14)返回TLR的实例,需要注意的是这个方法是静态方法,多个线程返回的是同一个TLR实例

  • 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相似,我们重点看下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变量。

总结
让每个线程内持有一个本地的种子变量,该种子变量只有在使用随机数时候才会被初始化,多线程下计算新种子时候是根据自己线程内维护的种子变量进行更新,从而避免了竞争

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值