Random和ThreadLocalRandom实现细节和注意事项

960 阅读2分钟

Random的使用

我们在开发中需要生成随机数的时候,大都会想到java.util.Random类的方法nextInt()快速生成指定范围内的随机数,示例如下。

Random random = new Random();
for (int i = 0; i < 9; i++) {
    // 生成 0-9 的随机整数
    random.nextInt(10);
}

Random源码实现

实例化Random过程中,在new Random()后,会生成一个和时间相关的种子。可以看到,无论是带参构造函数还是无参构造函数都是围绕初始化种子seed为主,我们接着往下看seed的作用。

//属性:种子
private final AtomicLong seed;

//构造函数
public Random() {
    this(seedUniquifier() ^ System.nanoTime());
}
//无参构造函数中使用到的方法
private static final AtomicLong seedUniquifier
    = new AtomicLong(8682522807148012L);
// 带参构造函数
public Random(long seed) {
    if (getClass() == Random.class)
        this.seed = new AtomicLong(initialScramble(seed));
    else {
        // subclass might have overriden setSeed
        this.seed = new AtomicLong();
        //方法体内依旧调用initialScramble,来设置种子
        setSeed(seed);
    }
}
//带参构造方法中使用到的方法
private static long initialScramble(long seed) {
    return (seed ^ multiplier) & mask;
}

种子seed的使用,主要是用于计算随机数,具体的使用看nextInt()实现。 代码的实现主要分为三步:

  • seed赋值给旧种子,通过旧种子计算出新种子
  • 通过compareAndSet以线程安全的方式将新种子替换旧种子
  • 通过新种子计算出随机数
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));
}

通过源代码可以看到对于种子seed,通过AtomicLong定义,通过compareAndSet来设置新种子,对于并发的线程,只有一个线程能够CAS成功,而失败的线程则通过自旋等待获取种子,保证了种子无并发问题。

但正是失败的线程通过自旋等待获取新种子,导致Random的使用存在一定的性能问题,这也正是ThreadLocalRandom在JDK1.7引入的原因。

ThreadLocalRandom的使用

ThreadLocalRandom实例的获取与Random有所不同,是通过静态方法current()进行获取的,示例代码如下所示。

ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current();
for (int i = 0; i < 9; i++) {
    // 生成 0-9 的随机数
    System.out.println(threadLocalRandom.nextInt(10));
}

ThreadLocalRandom存在的问题

在多线程中不能共享一个 ThreadLocalRandom 对象,否则会造成生成的随机数都相同

以上代码,ThreadLocalRandom的种子在current()初始化后(实例化不在此文章研究内容中,感兴趣的可以看下源码),在nextInt()方法执行过程中,才会再次更新,因此以下代码,存在一定的并发问题。

ExecutorService service = Executors.newCachedThreadPool();
// 共享 ThreadLocalRandom
ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current();
for (int i = 0; i < 10; i++) {
    // 多线程执行随机数并打印结果
    service.submit(() -> {
        System.out.println(Thread.currentThread().getName() + ":" + threadLocalRandom.nextInt(10));
        ;
    });
}

代码执行结果如下:

pool-1-thread-1:4   //线程1
pool-1-thread-2:4   //线程2
pool-1-thread-2:9   //线程2
pool-1-thread-1:9   //线程1
pool-1-thread-2:8   //线程2
pool-1-thread-2:5   //线程2
pool-1-thread-1:8   //线程1
pool-1-thread-1:5   //线程1
pool-1-thread-3:4
pool-1-thread-1:6

ThreadLocalRandom 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;
}
//通过当前种子计算新种子,并且新的种子,并依据当前线程更新保存起来
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;
}

通过计算新种子的代码就可以看出,在UNSAFE.getLong(t, SEED)UNSAFE.putLong(t = Thread.currentThread(), SEED, r = UNSAFE.getLong(t, SEED) + GAMMA)的操作下,当前线程根据实例化ThreadLocalRandom时初始化的种子,不断迭代执行相同的新种子计算以及替换旧种子操作。

因此出现了像以上实例中线程一和线程二随机出来的数字一样的情况。

但是!相较于Random, ThreadLocalRandom不存在线程争夺自旋等待的问题,种子保存在各自的线程当中,因此ThreadLocalRandom的执行效率是比Random高很多的