Java之Random和ThreadLocalRandom

前言

JDK7之前,java.util.Random都是使用比较广泛的随机数生成类,但是在多线程下的缺陷,也让JDK7之后再JUC包下新增了ThreadLocalRandom

Random相关

先从源码的角度来看看java.util.Random类的使用方法:

// 构造函数1:
public Random() {
    
    
	this(seedUniquifier() ^ System.nanoTime());
}

// 构造函数2:
public Random(long seed) {
    
    
  if (getClass() == Random.class)
    this.seed = new AtomicLong(initialScramble(seed));
  else {
    
    
    // subclass might have overriden setSeed
    this.seed = new AtomicLong();
    setSeed(seed);
  }
}

从构造函数2来看,使用Random类需要一个随机种子,如果不指定则会在默认构造函数1中生成一个默认的与系统时间相关的值,有了默认的种子后,如何生成随机数呢?

来看看nextInt(...)方法:

public int nextInt(int bound) {
    
    
  // 1. 校验参数,小于0则抛出异常
  if (bound <= 0)
    throw new IllegalArgumentException(BadBound);
  // 2. 获取下一个种子,该方法的代码在下面列出来
  int r = next(31);
  // 3. 根据新的种子计算随机数
  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;
}

由上面代码可知,新的随机数生成需要两个步骤:

  1. 根据老的种子生成新的种子
  2. 根据新生成的种子来计算新的随机数

那么问题来了,在单线程情况下,步骤2:int r = next(31)会按照期望的执行流程执行,先算出新的种子,接下来根据步骤3的固定逻辑算出新的随机数,

但是多线程情况下,会出现多个线程可能都拿同一个老的种子去执行步骤2,让后根据固定逻辑步骤3计算随机数,这样会导致多个线程算出来的随机数相同,这显然不是我们想要的。

所以步骤2必须要保证原子性,即当第一个线程在根据老的种子执行完步骤2计算出新的种子后,其他的线程应该把自己老的种子丢弃掉,然后使用第一个线程新生成的种子来计算自己的新种子,以此类推。

只有保证了这个,才能在保证多线程下产生的随机数是随机的,下面列出了相关代码

protected int next(int bits) {
    
    
  long oldseed, nextseed;
  AtomicLong seed = this.seed;
  // 使用旧的种子计算新的种子,这里使用了CAS
  do {
    
    
    oldseed = seed.get();
    nextseed = (oldseed * multiplier + addend) & mask;
  } while (!seed.compareAndSet(oldseed, nextseed));
  return (int)(nextseed >>> (48 - bits));
}

总结:每个Random实例里面都有一个原子性种子变量用来记录当前的种子值,当要生成新的随机数时,需要根据当前种子计算新的种子并更新回原子变量。在多线程下使用单个Random实例生成随机数时,多个线程会竞争同一个原子变量的更新操作,所以使用了CAS自旋操作,同时只有一个线程会成功,这样造成了大量的线程占用CPU资源空旋,降低了并发性能。

所以ThreadLocalRandom应运而生。

ThreadLocalRandom相关

先看看如何使用:

ThreadLocalRandom random = ThreadLocalRandom.current();
System.out.println(random.nextInt(5));

其中通过ThreadLocalRandom.current()来获取当前线程的随机数生成器。下面来分析ThreadLocalRandom的实现原理。

首先Random则是多个线程共享一个种子,所以在使用了非阻塞CAS来保证同时只有一个线程会成功。

Random多线程示例图.png

从名称上来看,ThreadLocalRandomThreadLocal相似:ThreadLocal通过让每一个线程复制一份变量,使得在每个线程对变量进行操作时实际上是操作自己本地内里面的副本,从而避免了对共享变量进行同步。

实际上ThreadLocalRandom的实现也是这个原理,而ThreadLocalRandomThreadLocalRandom对比来看,种子并不是放在ThreadLocalRandom自身中的一个属性,其实ThreadLocalRandom只是一个工具类,真正的种子则是放在了Thread中,这一点下面看源码的时候会讲。

ThreadLocalRandom多线程示例图.png

这样之后,每个线程只会维护自己的一个种子变量,每个线程只会根据自己老的种子生成新的种子,然后使用新的种子生成随机数,就不会存在竞争问题了,这里点个赞。

说到这里先看看ThreadLocalRandom的UML:

ThreadLocalRandomUML.png

由上图的UML来看,ThreadLocalRandom是继承了Random,但是并没有使用Random类中的seed原子种子变量,而是将种子放在了Thread中的threadLocalRandomSeed属性上,而且重写了nextInt方法。

上文提到过ThreadLocalRandom就是一个工具类,所以当线程调用current()时,会初始化调用线程的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 {
    
    
    // ThreadLocalRandom类也是属于java.rt包下的,通过bootstrap类加载器加载,所以可以直接获取
    UNSAFE = sun.misc.Unsafe.getUnsafe();
    Class<?> tk = Thread.class;
    // 获取Thread类里面threadLocalRandomSeed变量在Thread实例里面的偏移量
    SEED = UNSAFE.objectFieldOffset
      (tk.getDeclaredField("threadLocalRandomSeed"));
    // 获取Thread类里面threadLocalRandomProbe变量在Thread实例里面的偏移量
    PROBE = UNSAFE.objectFieldOffset
      (tk.getDeclaredField("threadLocalRandomProbe"));
    // 获取Thread类里面threadLocalRandomSecondarySeed变量在Thread实例里面的偏移量
    SECONDARY = UNSAFE.objectFieldOffset
      (tk.getDeclaredField("threadLocalRandomSecondarySeed"));
  } catch (Exception e) {
    
    
    throw new Error(e);
  }
}

current()方法

// instance是一个静态变量
static final ThreadLocalRandom instance = new ThreadLocalRandom();
public static ThreadLocalRandom current() {
    
    
  if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
    localInit();
  return instance; // 返回一个ThreadLocalRandom实例,静态变量
}

localInit()方法

static final void localInit() {
    
    
    int p = probeGenerator.addAndGet(PROBE_INCREMENT);
    int probe = (p == 0) ? 1 : p; // skip 0
    // 初始化时生成种子,并将并将种子通过Unsafe设置到Thread的threadLocalRandomSeed变量
    long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));
    Thread t = Thread.currentThread();
    UNSAFE.putLong(t, SEED, seed);
    UNSAFE.putInt(t, PROBE, probe);
}

这里需要说明的一点是,instance是一个静态变量,多个线程共享,所以在instance里面只是放置了一写线程安全的公共操作方法,所以不会涉及到线程安全问题。而在Thread中的threadLocalRandomSeed只是一个普通的long类型,这是因为每个线程维护一个属于自己的种子变量,不存在竞争了,这点原理和ThreadLocal相似。

生成随机数的方式和上文讲的Random的原理相似,所以每个线程在第一次调用current()方法初始化种子后,之后都会根据属于自己的旧的种子生成新的种子,然后通过新的种子生成新的随机数,并将新的种子重置设置在Thread实例中的threadLocalRandomSeed属性上:

// 生成新的种子并使用Unsafe的putLong更新到threadLocalRandomSeed属性
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生成随机数的原理和其在多线程下需要竞争原子种子变量更新操作的缺点,并分析了JDK7之后ThreadLocalRandom类与RandomThread之间的关系和原理,其让每个线程都持有了一个本地的种子变量,该种子变量只有在使用随机数的时候才会被初始化,多线程下计算新种子是根据自己所维护的种子变量进行更新,避免了多线程之间的竞争。

猜你喜欢

转载自blog.csdn.net/Lcumin/article/details/112003358