JUC中ThreadLocalRandom源码分析

ThreadLocalRandom 是 JDK7 在 JUC 包下新增的随机数生成器,它解决了 Random 在多线程下多个线程竞争内部唯一的原子性种子变量而导致大量线程自旋重试的不足。
需要注意的是 Random 本身是线程安全的。同时 Random 实例不是安全可靠的加密,可以使用 java.security.SecureRandom 来提供一个可靠的加密。

1. 随机数算法介绍

常用的随机数算法有两种:同余法(Congruential method)和梅森旋转算法(Mersenne twister)。Random 类中用的就是同余法中的一种 - 线性同余法(见Donald Kunth《计算机编程的艺术》第二卷,章节3.2.1)。

在程序中为了使表达式的结果小于某个值,常常采用取余的操作,结果是同一个除数的余数,这种方法叫同余法(Congruential method)。

线性同余法是一个很古老的随机数生成算法,它的数学形式如下:

Xn+1 = (a * Xn + c) % m

其中,m > 0, 0 < a < m, 0 < c < m

https://blog.csdn.net/lihui126/article/details/46236109

2. Random 源码分析

JDK 中的 Random 类生成的是伪随机数,使用的是 48-bit 的种子,然后调用线性同余方程,代码很简洁。

2.1 数据结构

private static final long multiplier = 0x5DEECE66DL;    // 相当于上面表达式中的 a
private static final long mask = (1L << 48) - 1;        // 相当于上面表达式中的 m
private static final long addend = 0xBL;                // 相当于上面表达式中的 c

// seed 生成的随机数种子
private final AtomicLong seed;

2.2 构造函数

// ^ 让 seed 更加随机
public Random() {
    this(seedUniquifier() ^ System.nanoTime());
}
public Random(long seed) {
    if (getClass() == Random.class)     
        // initialScramble 初始化的随机数
        this.seed = new AtomicLong(initialScramble(seed));
    else {
        this.seed = new AtomicLong();   // 子类重写 setSeed
        setSeed(seed);
    }
}

private static final AtomicLong seedUniquifier = new AtomicLong(8682522807148012L);
private static long seedUniquifier() {
    for (;;) {
        long current = seedUniquifier.get();
        long next = current * 181783497276652981L;
        if (seedUniquifier.compareAndSet(current, next))
            return next;
    }
}

// 初始化的随机数
private static long initialScramble(long seed) {
    return (seed ^ multiplier) & mask;
}

构造函数初始化了随机数种子 seed,之后的随机数都是在这个基础上进行计算的。 如果传入的 seed 值一样,那么生成的随机数也就是一样的了。

@Test
public void test() {
    long seed = 343L;
    Random random1 = new Random(seed);
    Random random2 = new Random(seed);

    Assert.assertEquals(random1.nextInt(), random2.nextInt());
    Assert.assertEquals(random1.nextInt(), random2.nextInt());
    Assert.assertEquals(random1.nextInt(), random2.nextInt());
}

2.3 生成随机数

public int nextInt() {
    return next(32);
}
public int nextInt(int bound) {
    if (bound <= 0)
        throw new IllegalArgumentException(BadBound);
    // 1. 生成随机数
    int r = next(31);
    int m = bound - 1;
    // 2. 生成的随机数不能超过 bound。   (n&-n)==n 也可以判断2^n
    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;
}

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

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

(1) 首先需要根据老的种子生成新的种子。
(2) 然后根据新的种子来计算新的随机数。

3. ThreadLocalRandom 源码分析

为了解决多线程高并发下 Random 的缺陷,JUC 包下新增了 ThreadLocalRandom 类。更多参考并发包中ThreadLocalRandom类原理剖析

3.1 ThreadLocalRandom 原理

@Test
public void testThreadLocalRandom() {
    ThreadLocalRandom random = ThreadLocalRandom.current();
    System.out.println(random.nextInt());
}

从名字看会让我们联想到基础篇讲解的 ThreadLocal,ThreadLocal 的出现就是为了解决多线程访问一个变量时候需要进行同步的问题,让每一个线程拷贝一份变量,每个线程对变量进行操作时候实际是操作自己本地内存里面的拷贝,从而避免了对共享变量进行同步

实际上 ThreadLocalRandom 的实现也是这个原理,Random 的缺点是多个线程会使用同一个原子性种子变量,会导致对原子变量更新的竞争。那么如果每个线程维护自己的一个种子变量,每个线程生成随机数时候根据自己老的种子计算新的种子,并使用新种子更新老的种子,然后根据新种子计算随机数,就不会存在竞争问题。这会大大提高并发性能,如下图 ThreadLocalRandom 原理:

3.2 数据结构

ThreadLocalRandom类图

从 ThreadLocalRandom 类图中可以看到和 Random 保存一份 seed 不同,ThreadLocalRandom 的种子变量保存在 Thread.threadLocalRandomSeed 变量中,通过 Unsafe 操作这个变量。关于 threadLocalRandomSeed、threadLocalRandomProbe、threadLocalRandomSecondarySeed 这三个变量,Thread 类有相关的注释:

/** The current seed for a ThreadLocalRandom */
// 1. 和 Random 中的 seed 类似
long threadLocalRandomSeed;

/** Probe hash value; nonzero if threadLocalRandomSeed initialized */
// 2. 在 CurrentHashMap 中有使用。probe 是探测的意思,
int threadLocalRandomProbe;

/** Secondary seed isolated from public ThreadLocalRandom sequence */
int threadLocalRandomSecondarySeed;

需要注意的是这三个值都不能为 0,因为 0 在 ThreadLocalRandom 中有特殊的含义,表示还未初始化,调用 localInit() 进行初始化

3.3 构造函数

boolean initialized;
private ThreadLocalRandom() {
    initialized = true; // false during super() call
}
public static ThreadLocalRandom current() {
    if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
        localInit();
    return instance;
}

ThreadLocalRandom 构造函数为私有的,只能通过 current 方法构建,如果 PROBE 还是默认值 0 表示未初始化,调用 localInit 进行初始化。

3.4 生成随机数 nextInt

// Random 一样也有两步:一是根据老的种子生成新的种子;
//                     二是根据新的种子来计算新的随机数
public int nextInt() {
    return mix32(nextSeed());
}

public int nextInt(int bound) {
    if (bound <= 0)
        throw new IllegalArgumentException(BadBound);
    int r = mix32(nextSeed());
    // 1. 生成随机数
    int m = bound - 1;
    // 2. 生成的随机数不能超过 bound
    // 2.1 bound 是 z^n 则直接取余
    if ((bound & m) == 0) // power of two
        r &= m;
    // 2.2取 [0, bound] 之间的数
    else { // reject over-represented candidates
        for (int u = r >>> 1; u + m - (r = u % bound) < 0; u = mix32(nextSeed()) >>> 1)
            ;
    }
    return r;
}

ThreadLocalRandom 和 Random 一样也有两步:

(1) 根据老的种子生成新的种子;
(2) 根据新的种子来计算新的随机数。

nextInt(int bound) 和 nextInt 的思路是一样的,先调用 mix32(nextSeed()) 函数生成随机数(int类型的范围),再对参数 n 进行判断,如果 n 恰好为 2 的方幂,那么直接移位就可以得到想要的结果;如果不是 2 的方幂,那么就关于 n 取余,最终使结果在[0,n)范围内。另外,for 循环语句的目的应该是防止结果为负数。

当bound为2n时, bound与生成的随机数相乘, 相当于取随机数的前log2bound
其它情况时, 将int的取值范围231−1以bound为区间范围划分为n组, 最后一个区间的数不够bound个, 如果生成的随机数是从这个区间内生成的, 则难以保证随机性, 故需要重新生成.

// 生成新的种子,保存在 Thread.threadLocalRandomSeed 中。 GAMMA=0x9e3779b97f4a7c15L
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 一样了
private static int mix32(long z) {
    z = (z ^ (z >>> 33)) * 0xff51afd7ed558ccdL;
    return (int)(((z ^ (z >>> 33)) * 0xc4ceb9fe1a85ec53L) >>> 32);
}

3.5 其它方法

(1) getProbe

getProbe 用法参考 ConcurrentHashMap#fullAddCount 方法。

static final int getProbe() {
    return UNSAFE.getInt(Thread.currentThread(), PROBE);
}

static final int advanceProbe(int probe) {
    probe ^= probe << 13;   // 异或位运算。 xorshift
    probe ^= probe >>> 17;
    probe ^= probe << 5;
    UNSAFE.putInt(Thread.currentThread(), PROBE, probe);
    return probe;
}

(2) nextSecondarySeed

static final int nextSecondarySeed() {
    int r;
    Thread t = Thread.currentThread();
    if ((r = UNSAFE.getInt(t, SECONDARY)) != 0) {
        r ^= r << 13;   // xorshift
        r ^= r >>> 17;
        r ^= r << 5;
    }
    else {
        localInit();
        if ((r = (int)UNSAFE.getLong(t, SEED)) == 0)
            r = 1; // avoid zero
    }
    UNSAFE.putInt(t, SECONDARY, r);
    return r;
}

参考:

  1. 并发包中ThreadLocalRandom类原理剖析
  2. 《ThreadLocalRandom和Random性能测试》:http://www.importnew.com/12460.html
  3. 《Java中的random函数是如何实现的》:https://my.oschina.net/hosee/blog/600392
  4. 《解密随机数生成器》:https://blog.csdn.net/lihui126/article/details/46236109
  5. 多线程下ThreadLocalRandom用法 - 简书 (jianshu.com)  
posted @ 2021-10-21 11:08  Chen洋  阅读(133)  评论(0编辑  收藏  举报