Random在多线程下的问题以及ThreadLocalRandom类分析

Ramdom在多线程环境下的问题

首先我们看一下Random类的nextInt源码:

//产生[0,bound)的随机数
public int nextInt(int bound) {
    if (bound <= 0)
        throw new IllegalArgumentException(BadBound);
    //生成一个31位的随机数
    int r = next(31);
    int m = bound - 1;
    // 如果bound是2的整数幂次方
    if ((bound & m) == 0)  // i.e., bound is a power of 2
        r = (int)((bound * (long)r) >> 31);
    // 通过除余运算产生一个小于bound的值
    else {
        for (int u = r;
                u - (r = u % bound) + m < 0;
                u = next(31))
            ;
    }
    return r;
}

可以看出,产生随机数的核心逻辑在于next方法中,并且实际上Random类的其他常用方法都是基于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类的内部维护了一个随机数种子,并且类型为AtomicLong,那为什么要用AtomicLong呢?

可以这样想:在多线程环境下,多个线程同时调用next方法,同时获取旧种子并使用固定的算法生成新种子。那么每个线程产生的新种子的值自然是相同的,产生的随机数也是相同的,这当然是不希望看到的。

于是,Random类将种子定义为AtomicLong类型,并通过CAS自旋锁的方式去更改随机数种子的值,若多个线程同时获取旧种子的值,并产生相同的新种子,但只要有一个线程通过CAS设置了新种子的值,后面的线程就无法通过CAS进行更改,必须重新获取种子的值并生成新种子。

通过CAS自旋之后,多线程下的问题是解决了,但是却产生了效率问题:多个线程争抢一个原子变量,只有一个线程会成功,而其他线程会在CAS上自旋重试,影响了并发性能。

ThreadLocalRandom分析

ThreadLocalRandom是JDK1.7为了解决Random并发效率问题产生的类,先来看看ThreadLocalRandom怎么用:

public static void main(String[] args) {
    Runnable runnable = ()->{
        // 通过静态方法获取ThreadLocalRandom对象
        ThreadLocalRandom random = ThreadLocalRandom.current();

        System.out.println(random.nextInt(100));
    };

    for(int i = 0;i<10;i++){
        new Thread(runnable).start();
    }
}

ThreadLocalRandom.current()获取的到底是不是同一个对象呢?看看源码:

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

PROBE是什么东西呢?可以在ThreadLocalRandom中找到如下静态字段和静态代码块:

// Unsafe mechanics
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.objectFieldOffset这个方法就厉害了,是获取指定的变量在所属类中的内存偏移地址,查看Thread源码,可以看到这些字段:

/** The current seed for a ThreadLocalRandom */
@sun.misc.Contended("tlr")
long threadLocalRandomSeed;

/** Probe hash value; nonzero if threadLocalRandomSeed initialized */
@sun.misc.Contended("tlr")
int threadLocalRandomProbe;

/** Secondary seed isolated from public ThreadLocalRandom sequence */
@sun.misc.Contended("tlr")
int threadLocalRandomSecondarySeed;

其中threadLocalRandomSeed是线程持有的随机数种子,threadLocalRandomProbe可以范围为探针,大概是用来辅助的,threadLocalRandomSecondarySeed是备用的随机数种子。

@sun.misc.Contended注解是避免伪共享问题进行字段填充。

于是回到current方法:

public static ThreadLocalRandom current() {
    //getInt是通过偏移地址获取对象中的字段值
    //如果为0说明还没初始化过,进行初始化
    if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
        localInit();
    return instance;
}

这里注意到其实return的对象是一个内部的静态对象,是单例的:

static final ThreadLocalRandom instance = new ThreadLocalRandom();

那么localInit方法初始化的是什么呢?联系上面看到的Thread中的字段可以猜想也许是初始化线程中自己持有的Seed、Probe等:

static final void localInit() {
    int p = probeGenerator.addAndGet(PROBE_INCREMENT);
    //probe不能等于0
    int probe = (p == 0) ? 1 : p; // skip 0
    //产生随机数种子
    long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));
    Thread t = Thread.currentThread();
    // 将probe和seed设置到当前线程的字段中
    UNSAFE.putLong(t, SEED, seed);
    UNSAFE.putInt(t, PROBE, probe);
}

其中probeGeneratorseeder都是一个AtomicInteger类型,通过addAndGet一个固定的偏移量为每个线程产生不同的字段取值(seed还要再经过混淆)。

可以看到,实际上,ThreadLocalRandom是让各个线程持有了自己的随机数种子,这点和ThreadLocal很像。

然后再看一下nextInt方法:

public int nextInt(int bound) {
    if (bound <= 0)
        throw new IllegalArgumentException(BadBound);
    //产生新的种子后用种子做混淆
    int r = mix32(nextSeed());
    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;
}

通过nextSeed产生新的种子:

final long nextSeed() {
    Thread t; long r; // read and update per-thread seed
    UNSAFE.putLong(t = Thread.currentThread(), SEED,
                    r = UNSAFE.getLong(t, SEED) + GAMMA);
    return r;
}

其他逻辑和Random基本一样。通过以上分析可以看出,线程通过ThreadLocalRandom.current()获取的是同一个ThreadLocalRandom对象,但是因为各个线程内部维护的随机数种子不一样(第一次调用current会触发初始化),所以产生的随机数也会不一样。

原创文章,作者:彭晨涛,如若转载,请注明出处:https://www.codetool.top/article/random%e5%9c%a8%e5%a4%9a%e7%ba%bf%e7%a8%8b%e4%b8%8b%e7%9a%84%e9%97%ae%e9%a2%98%e4%bb%a5%e5%8f%8athreadlocalrandom%e7%b1%bb%e5%88%86%e6%9e%90/

发表评论

电子邮件地址不会被公开。