Random和ThreadLocalRandom原理分析

13 篇文章 2 订阅

Random和ThreadLocalRandom都是用于生成随机数的。用法比较简单,这里直接看代码分析原理。

Random

Random随机数的实现几乎都是依赖于内部的next方法,都是在next的基础上进行额外的计算,这里直接看next实现:

protected int next(int bits) {
    long oldseed, nextseed;
    AtomicLong seed = this.seed;
    do {
    	//获得老的种子数据
        oldseed = seed.get();
        //通过老的种子计算新的种子
        nextseed = (oldseed * multiplier + addend) & mask;
     //用新的种子替换老的种子
    } while (!seed.compareAndSet(oldseed, nextseed));
    //返回新的种子
    return (int)(nextseed >>> (48 - bits));
}

Random获取随机数分两步:

  1. 获得老的种子数据
  2. 通过老的种子计算新的种子

最后,用老的种子替换新的种子,这个我们都能理解,但是这里使用的是CAS进行替换,这个是为什么呢?

如果不用CAS,再看上面代码,如果是多个线程同时并行访问,会出现:多个线程同时获得了同一个老的种子,然后根据老的种子计算出同样的新的种子,这就导致了多个线程获得的随机数相同。

所以,介于这种情况,使用CAS来进行控制,使得在多线程的情况下,保证每个线程获取的随机数是不同的。如果CAS失败,说明老的种子已经被使用,所以会继续循环重新拿到最新的老的种子再去计算新的种子,直到成功并返回。

Random确实很好的解决了多线程下的问题,但是这又带来了新的问题:在线程竞争激烈的情况下,多个线程通过CAS竞争一个seed,导致大多数线程都在自旋,严重降低了多线程下获取随机数的效率,所以ThreadLocalRandom就是为了解决这个问题。

ThreadLocalRandom

初始化

ThreadLocalRandom的原理其实也很简单。既然Random多线程共享seed,竞争激烈导致效率低,那为什么不每个线程都拥有自己的seed呢,每个线程每次通过自己的当前seed获取新的seed呢?是的,ThreadLocalRandom就是这样做的。

那每个线程如果都拥有自己的seed,首先每个线程的seed保存在哪儿呢?不用多想,当前是当前线程对象了(ThreadLocal在线程本地也有自己的ThreadLocalMap,原理是类似的)。
为了实现ThreadLocalRandom,在Thread中有如下字段:

 /** The current seed for a ThreadLocalRandom */
 //用于产生随机数的种子
 @sun.misc.Contended("tlr")
 long threadLocalRandomSeed;
 /** Probe hash value; nonzero if threadLocalRandomSeed initialized */
 //如果threadLocalRandomSeed被初始化则threadLocalRandomProbe不为0
 @sun.misc.Contended("tlr")
 int threadLocalRandomProbe;
 /** Secondary seed isolated from public ThreadLocalRandom sequence */
 @sun.misc.Contended("tlr")
 //用来生成threadLocalRandomSeed的辅助种子
 int threadLocalRandomSecondarySeed;

从Thread中字段的定义来看,threadLocalRandomSeed是没有初始值得。可想而知:只有用到ThreadLocalRandom的时候,才需要threadLocalRandomSeed,需要的时候再进行初始化,避免不必要的内存消耗。

ThreadLocalRandom是在第一次调用current的时候,对当前threadLocalRandomSeed进行的初始化的,并且ThreadLocalRandom是单例模式:

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

对上面代码中的UNSAFE.getInt(Thread.currentThread(), PROBE)不理解的,这里简单讲一下unsafe相关的知识。先看这句代码,找到UNSAFE和PROBE到底是什么,在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 = sun.misc.Unsafe.getUnsafe();
        Class<?> tk = Thread.class;
        SEED = UNSAFE.objectFieldOffset
            (tk.getDeclaredField("threadLocalRandomSeed"));
        PROBE = UNSAFE.objectFieldOffset
            (tk.getDeclaredField("threadLocalRandomProbe"));
        SECONDARY = UNSAFE.objectFieldOffset
            (tk.getDeclaredField("threadLocalRandomSecondarySeed"));
    } catch (Exception e) {
        throw new Error(e);
    }
}

可以看到,UNSAFE的类型是sun.misc.Unsafe,而PROBE,通过UNSAFE.objectFieldOffset获取的。其实,在unsafe中,定义了很多的直接内存访问操作,操作更高效,但是也存在一定的风险。unsafe可以通过getDeclaredField获得某个对象中某个字段的偏移量,然后可以直接通过字段偏移量获取某个对象的字段值,也可以通过字段偏移量给对象的字段赋值。当然,unsafe的功能不止于此,对于unsafe更详细的介绍可以直接看 Unsafe应用解析
这里就是,先通过static初始化拿到线程中threadLocalRandomSeed等变量的偏移量,然后在用到这些变量的时候,通过初始化得到的偏移量,使用get和put取值和赋值操作。

好,现在继续回过头看ThreadLocalRandom的创建:

public static ThreadLocalRandom current() {
	//拿到当前线程对象中threadLocalRandomProbe的值,如果为0(默认值就是0),说明初始种子没有初始化,所以需要先进行初始化。
    if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
        localInit();
   	//instance是static,ThreadLocalRandom 是单例模式,因为seed保存在当前的线程中,所以没必要每次都创建一个ThreadLocalRandom 
    return instance;
}
static final void localInit() {
	//重新计算threadLocalRandomProbe,因为已经初始化了,所以threadLocalRandomProbe一定不能为0了。
    int p = probeGenerator.addAndGet(PROBE_INCREMENT);
    int probe = (p == 0) ? 1 : p; // skip 0
    //计算初始种子,这里关键是seeder。
    //seeder是静态变量,在类加载的时候会初始化一个最初始的值。
    //这里在计算seed的值得同时,每来一个线程进行初始化,这里都会为seeder的值加SEEDER_INCREMENT,保证下一个线程的seeder和前一个的不一样。
    //所以当所有线程第一次同时来竞争初始化的时候,会存在竞争,初始化之后,就不会再竞争了。
    long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));
    Thread t = Thread.currentThread();
    //将计算后的初始化值通过unsafe赋值给当前线程的变量
    UNSAFE.putLong(t, SEED, seed);
    UNSAFE.putInt(t, PROBE, probe);
}

初始化种子,关键是seeder,seeder定义与初始化如下:

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

private static long initialSeed() {
	//这儿可以配置启动参数来选择通过哪种方式进行初始化seeder。
    String sec = VM.getSavedProperty("java.util.secureRandomSeed");
    if (Boolean.parseBoolean(sec)) {
        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是静态变量,所以在类加载的时候,就初始化好了。因为每个线程调用localInit都会改变seeder的值,也就保证了每个线程的seeder是不一样的。

nextSeed

ThreadLocalRandom获取随机数,基本都是通过nextSeed实现的,nextSeed代码如下,和Random原理相同,分为三步:

final long nextSeed() {
    Thread t; long r; // read and update per-thread seed
    //1、获取老的种子
    //2、计算新的种子
    //3、将新的种子覆盖老的种子
    UNSAFE.putLong(t = Thread.currentThread(), SEED,
                   r = UNSAFE.getLong(t, SEED) + GAMMA);
    return r;
}
  1. 获取老的种子 : UNSAFE.getLong(t, SEED)
  2. 计算新的种子:r = UNSAFE.getLong(t, SEED) + GAMMA
  3. 将新的种子覆盖老的种子 UNSAFE.putLong(t = Thread.currentThread(), SEED, r = UNSAFE.getLong(t, SEED) + GAMMA);

和Random的区别就是:种子保存在当前线程对象中。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值