月泉的博客

JUC-ThreadLocalRandom原理剖析

月泉 并发编程JUC

JUC-ThreadLocalRandom原理剖析

ThreadLocalRandom是什么

在Java中生成随机数通常都会使用Random来生成随机数,用法如下:

public static void main(String[] args) {
    Random random = new Random();
    for (int i = 0; i < 10; i++) {
        System.out.println(random.nextInt(5));
    }
}

上述代码会生成0-4的十次随机数,那么Random在多线程环境下使用会有什么问题呢?这首先要观其源码得知背后。

public int nextInt(int bound) {
    if (bound <= 0)
        throw new IllegalArgumentException(BadBound);

    int r = next(31);
    int m = bound - 1;
    if ((bound & m) == 0)  // i.e., bound is a power of 2
        r = (int)((bound * (long)r) >> 31);
    else {
        for (int u = r;
             u - (r = u % bound) + m < 0;
             u = next(31))
            ;
    }
    return r;
}

初略的看一下, 首先是判断传入的bound是否满足条件不满足则抛出IllegalArgumentException,然后调用next(31)去获得种子,然后生成随机数,接着看下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));
}

首先定义了2个变量,一个为旧的种子一个为新的种子,首先从当前实例中获取旧的种子,然后通过自旋CAS的方式将旧的种子替换成新值,最终返回新的种子。

从上述源码不难看出Random在同一个实例下在多线程运用中暴露出来的问题,首先旧的种子可能同时被多个线程同时持有导致随机性下降,其次采用自旋CAS来修改实例中seed的旧种子所以可能导致多次自旋在频繁时可能会长时自旋也是有可能的,在这个背景下ThreadLocalRadom就应运而生了(JDK1.7新增)。

ThreadLocalRandom的使用

先观其用法:

public static void main(String[] args) {
    ThreadLocalRandom random = ThreadLocalRandom.current();
    for (int i = 0; i < 10; i++) {
        System.out.println(random.nextInt(5));
    }
}

用法也很简单,不难看出来,先通过ThreadLocalRadom的静态方法current获取一个ThreadLocalRadom的实例,然后再通过next*来获取一个随机数

ThreadLocalRandom的原理

ThreadLocalRadom根据名字不难看出来其和ThreadLocal是有所关联的,那么既然是共享资源“种子”所产生的问题,那么能不能利用ThreadLocal的概念让每个线程都持有一个自己的种子呢?,这就是ThreadLocalRadom的核心思想了,它本质上和ThreadLocal一样就是一层壳。

接着用代码说话首先看下ThreadLocalRandom类的结构

public class ThreadLocalRandom extends Random {
    static {
        ......
    }
    
}

首先该类是继承至Random,然后重写了next*ints(...)longs(...)doubles(...)等,接着我们仅关注核心的几个,首先看下静态块做了什么。

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实例,然后再拿Thread类实例,然后从类实例中获取threadLocalRandomSeedthreadLocalRandomProbethreadLocalRandomSecondarySeed字段的偏移量

接着再看current方法

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

发现无论多少个线程实例化都会只有相同的实例,这是一个典型的单例,接着看那句if实际上就是获取当前线程对象的PROBE的偏移量的值是不是等于0,如果等于0证明这个线程没有拥有过种子,接着就调用localInit()

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

从上述代码不难看出来,是给当前线程对象中的SEED也就是种子赋值以及将PROBE改为1,从而给线程对象初始化一个SEED

接着看在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;
}

和父类RandomnextInt类似,都是拿到种子然后生成随机数,接着关注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;
}

实现很简单,就是拿到当前线程对象根据SEED的偏移量拿到值然后再生成新的种子然后返回。

总结

ThreadLocalRadom会在一个线程第一次使用的时候给线程对象初始化种子,在使用时利用unsafe中提供的硬件操作,对线程对象中的值取值和赋值,让每个线程对象都有自己的种子,这样就规避掉了Random中多个线程持有一个random实例导致数据竞争让多个线程都竞争同一个种子造成的数据的随机性下降和自旋CAS操作。

月泉
伪文艺中二青年,热爱技术,热爱生活。