Java源码分析:产生随机数Random与ThreadLocalRandom的区别

时间:2022-06-01 18:17:34

Java用于产生随机数的方法主要有两种:java.util.Random和java.util.concurrent.ThreadLocalRandom。

Random从Jdk 1.0开始就有了,而ThreadLocalRandom是Jdk1.7才新增的。简单从命名和类所在的包上看,两者的区别在于对并发的支持。

Random

Random是一个伪随机数生成器,它内置了一个种子数seed。

获取随机数的源码

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,在每一次获取随机数执行next(int bits)时,按固定的算法更新seed值,从而达到伪随机的效果。

固定更新种子的算法为:

nextseed = (oldseed * multiplier + addend) & mask

换句话说,只要初始的种子是相同的,Random得到的随机数序列是相同的。

示例

Random random = new Random();
random.setSeed(1);
System.out.println(random.nextInt(10));
System.out.println(random.nextInt(10));
random.setSeed(1);
System.out.println(random.nextInt(10));
System.out.println(random.nextInt(10));

输出的结果为:

5
8
5
8

对于种子为1得到的随机序列均为5,8

Random线程安全的实现

JDK1.7新增ThreadLocalRandom是不是因为Random获取随机数不是线程安全的呢?

Random内置的种子使用了 AtomicLong类型,Random在执行next(int bits)获取种子,更新种子是线程安全的。所以在多线程使用同一个Random实例获取随机数是线程安全的。

问题也正是Random实现线程安全的方案上,在多线程并发环境下,各个线程在每一次产生随机数时都需要竞争获取种子。

ThreadLocalRandom

ThreadLocalRandom继承于Random,与Random不同的地方时,它提供了不一样的多线程并发实现方案。

ThreadLocalRandom使用了单例模式实现,使用ThreadLocalRandom.current()方法来获取ThreadLocalRandom实例。

current()方法

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

获取ThreadLocalRandom实例时,首先检查当前线程的PROBE的值是否为0,否则调用localInit

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

localInit()方法做了这几件事:

  1. probeGenerator类型为AtomicInteger,它会产生一个非0的probe的值
  2. 种子产生器seeder类型为AtomicLong,它生成一个新的种子seed
  3. 对当前线程设置SEED和PROBE的值。

概括说,这段代码是对当前线程初始一个种子seed,使用非0值的probe来标记当前线程是否初始了种子。

也就是说,ThreadLocalRandom在获取实例时,给线程初始化了不同的种子。与Random不同的地方是,竞争种子只有在获取ThreadLocalRandom那一刻才会出现,在获取随机数的next()操作时是不会出现线程竞争。

public int nextInt() {
    return mix32(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;
}

种子是long类型,nextSeed在更新种子是在当前线程下,不会出现线程竞争。

setSeed()

Random是可以对实例初始种子,也可以使用setSeed()方法重置种子。而ThreadLocalRandom则不支持此操作:

public void setSeed(long seed) {
    // only allow call from super() constructor
    if (initialized)
        throw new UnsupportedOperationException();
}

可以看到ThreadLocalRandom是禁止了setSeed()方法。

总结

Random和ThreadLocalRandom的最大区别在于对并发的处理上不同:

  1. Random是在每次获取随机数时需要竞争种子的更新
  2. ThreadLocalRandom则是在获取ThreadLocalRandom实例时才需要竞争种子的更新,大大降低种子更新竞争的频率。