关于juc:Java-JUC-ThreadLocalRandom类解析

40次阅读

共计 6534 个字符,预计需要花费 17 分钟才能阅读完成。

ThreadLocalRandom 类解析

前言

ThreadLocalRandom 类是 JDK7 在 JUC 包下新增的随机数生成器,它次要解决了 Random 类在多线程下的有余。

本文次要解说为什么须要 ThreadLocalRandom 类,以及该类的实现原理。

Random 类及其局限性

首先咱们先理解一下 Random 类。在 JDK7 以前到当初,java.util.Random 类都是应用较为宽泛的随机数生成工具类,而且 java.lang.Math 的随机数生成也是应用的 java.util.Random 类的实例,上面先看看如何应用 Random 类。

public static void main(String[] args) {
        //1. 创立一个默认种子随机数生成器
        Random random = new Random();
        //2. 输入 10 个在 0 - 5 之间的随机数(蕴含 0, 不蕴含 5)
        for (int i = 0; i < 10; i++) {System.out.print(random.nextInt(5)); //3421123432
        }
}

随机数的生成须要一个 默认种子 ,这个种子其实是一个long 类型的数字,能够通过在创立 Random 类对象时通过构造函数指定,如果不指定则在默认构造函数外部生成一个默认的值。

种子数只是随机算法的起始数字,和生成的随机数字的区间无关。

public Random() {this(seedUniquifier() ^ System.nanoTime());
}
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);
        }
}

在有了默认种子之后,Random 是如何生成随机数的呢?咱们看一下 nextInt()办法。

public int nextInt(int bound) {
              //3. 参数查看
        if (bound <= 0)
            throw new IllegalArgumentException(BadBound);
                //4. 依据老的种子生成新的种子
        int r = next(31);
              //5. 依据新的种子计算随机数
        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;
}

由此可见,新的随机数的生成须要两个步骤:

  • 依据老的种子生成新的种子
  • 而后依据新的种子来计算新的随机数

其中步骤 4 能够形象为seed = f(seed),比方seed = f(seed) = a*seed+b

步骤 5 能够形象为g(seed,bound),比方g(seed,bound) = (int) ((bound * (long) seed) >> 31)

在单线程状况下每次调用 nextInt() 都是依据老的种子计算出新的种子,这是能够保障随机数产生的随机性。但在多线程下多个线程可能 拿同一个老的种子 去执行步骤 4 以计算新的种子,这会导致多个线程的新种子是一样的,并且因为步骤 5 的算法是固定的,所以会导致 多个线程产生雷同的随机值

所以步骤 4 要保障 原子性,也就是说当多个线程依据同一个老种子计算新种子时,第一个线程的新种子被计算出来后,第二个线程要抛弃本人老的种子,而应用第一个线程的新种子来计算本人的新种子,依此类推。

💡 在 Random 中应用了 原子变量 AtomicLong来达到这个成果,在创立 Random 对象时初始化的种子就被保留到了种子原子变量外面

接下来看一下 next()办法:

protected int next(int bits) {
        long oldseed, nextseed;
        AtomicLong seed = this.seed;
        do {
            //6. 获取以后变量种子
            oldseed = seed.get();
            //7. 依据以后种子变量计算新的种子
            nextseed = (oldseed * multiplier + addend) & mask;
          //8. 应用 CAS 操作,它应用新的种子去更新老的种子,失败的线程会通过循环从新获取更新后的种子作为以后种子去计算老的种子
        } while (!seed.compareAndSet(oldseed, nextseed));
        return (int)(nextseed >>> (48 - bits));
}

总结:在多线程下尽管同一时刻只有一个线程会胜利,然而会造成大量线程进行自旋操作,这样会升高并发性能,所以 ThreadLocalRandom 使用而生。

ThreadLocalRandom

为了补救 Random 在多线程下的效率问题,在 JUC 包中减少了 ThreadLocalRandom 类,上面先演示如何应用 ThreadLocalRandom 类:

public static void main(String[] args) {
        //1. 获取一个随机数生成器
        ThreadLocalRandom random = ThreadLocalRandom.current();
        //2. 输入 10 个在 0 -5(蕴含 0, 不蕴含 5)之间的随机数
        for (int i = 0; i < 10; ++i) {System.out.print(random.nextInt(5));
        }

}

其中第一步调用 ThreadLocalRandom.current() 来获取 以后线程 的随机数。

实际上 ThreadLocalRandom 的实现和 ThreadLocal 差不多,都是通过让每一个线程复制一份变量,从而让每个线程实际操作的都是本地内存中的正本,防止了对共享变量的同步。

Random 的毛病是多个线程会应用同一个原子性种子变量,从而导致对原子变量更新的竞争。

而在 ThreadLocalRandom 中,每个线程都保护一个种子变量,在每个线程生成随机数的时候都依据以后线程中旧的种子去计算新的种子,并应用新的种子更新老的种子,再依据新的种子去计算随机数,这样就不会存在竞争问题了。

源码解析

首先咱们先看一下 ThreadLocalRandom 的类图构造。

从图中能够看到 ThreadLocalRandom 继承了 Random 类并重写了 nextInt() 办法,在 ThreadLocalRandom 类中并没有应用 Random 的原子性种子变量。

在 ThreadLocalRandom 中并没有寄存具体的种子,具体的种子都放在具体的调用线程的 ThreadLocalRandomSeed 中变量中。

ThreadLocalRandom 相似于 ThreadLocal 类,是个工具类。当线程调用 ThreadLocalRandom 的 current()办法时,ThreadLocalRandom 负责初始化调用线程的 threadLocalRandomSeed 变量,进行初始化种子。

在调用 ThreadLocalRandom 的 nextInt() 办法时,理论就是获取以后线程的 ThreadLocalRandomSeed 变量作为以后种子去计算新种子,而后更新新的种子到 ThreadLocalRandomSeed 中,随后再依据新的种子去计算随机数。

须要留神的是:threadLocalRandomSeed 变量就是 Thread 类中的一个一般 long 类型的变量,不是原子类型变量。

@sun.misc.Contended("tlr")
long threadLocalRandomSeed;

因为这个变量是线程级别的,基本不须要应用原子类型变量。

ThreadLocalRandom 中的 seeder 和 probeGenerator 是两个原子性变量,将 probeGenerator 和 seeder 申明为原子变量的目标是为了在多线程状况下,赋予它们各自不同的种子初始值,这样就不会导致每个线程产生的随机数序列都是一样的,而且 probeGenerator 和 seeder 只会在初始化在初始化调用线程的种子和探针变量 (用于扩散计算数组索引下标) 时候用到,每个线程只会应用一次。

另外变量 instance 是 ThreadLocalRandom 的一个实例,该变量是 static,多个线程应用的实例是同一个,然而因为 具体的种子存在在线程外面的,所以在 ThreadlocalRandom 的实例外面只蕴含线程无关的的通用算法,因而它是线程平安的

上面来看一下 ThreadLocalRandom 类的次要代码逻辑:

1.Unsafe 机制

    // Unsafe mechanics
    private static final sun.misc.Unsafe UNSAFE;
    private static final long SEED;
    private static final long PROBE;
    private static final long SECONDARY;
    static {
        try {
              // 获取实例
            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);
        }
    }

2.ThreadLocalRandom current()办法

该办法获取 ThreadLocalRandom 实例,并初始化调用线程中的 threadLocalRandomSeed、threadLocalRandomProbe 变量。

static final ThreadLocalRandom instance = new ThreadLocalRandom();
public static ThreadLocalRandom current() {if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
            localInit();
        return instance;
}
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);
}

在下面代码中,如果以后线程中的 threadLocalRandomProbe 的变量值为 0(默认状况下线程的这个变量为 0),则阐明以后线程是第一次调用 current() 办法,那么须要调用 localInit() 办法计算以后线程的初始化种子变量。

这里为了提早初始化,在不须要随机数性能的时候就不初始化 Thread 类中的种子变量。

localInit() 中,首先依据 probeGenerator 计算以后线程中的 threadLocalRandomProbe 初始化值,而后依据 seeder 计算以后线程的初始化种子,而后把这两个变量设置到以后线程中。

在 current 最初返回 ThreadLocalRandom 的实例。须要留神的是,这个办法是静态方法,多个线程返回的是同一个 ThreadLocalRandom 实例

3.int nextInt(int bound)办法

计算以后线程的下一个随机数。

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

下面逻辑和 Random 相似,重点是 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;
}

在这段代码中,首先应用变量 r = UNSAFE.getLong(t,SEED) 后去获取以后线程中 threadLocalRandomSeed 变量的值,而后在种子的根底上累加 GAMMA 值作为新的种子,之后应用 UNSAFE 的 putLong 办法 把新的种子放入以后线程 threadLocalRandomSeed 变量中。

4.initialSeed

private static final AtomicLong seeder = new AtomicLong(initialSeed());

    private static long initialSeed() {String sec = VM.getSavedProperty("java.util.secureRandomSeed");
        if (Boolean.parseBoolean(sec)) {byte[] seedBytes = java.security.SecureRandom.getSeed(8);
            long s = (long)(seedBytes[0]) & 0xffL;
            for (int i = 1; i < 8; ++i)
                s = (s << 8) | ((long)(seedBytes[i]) & 0xffL);
            return s;
        }
        return (mix64(System.currentTimeMillis()) ^
                mix64(System.nanoTime()));
}

在初始化种子变量的初始值对应的原子变量 seeder 时,调用了 initialSeed() 办法,首先判断 java.util.secureRandomSeed 的零碎属性值是否为 true 来判断是否应用安全性高的种子,如果为 true 则应用 java.security.SecureRandom.getSeed(8) 获取高安全性种子,如果为 false 则依据以后工夫戳来获取初始化种子,也就是说应用安全性高的种子是无奈被预测的,而 Random、ThreadLocalRandom 产生的被称为“伪随机数”,因为是可被预测的。

总结

ThreadLocalRandom 应用 ThreadLocal 的原理,让每个线程都持有一个本地的种子变量,该种子变量只有在应用随机数时才会被初始化。在多线程下计算新种子时是依据本人线程内保护的种子变量进行更新,从而防止了竞争。

正文完
 0