java并发编程(4)-Random以及ThreadLocalRandom原理剖析

194 阅读5分钟

java并发编程系列前文

  1. java并发编程(1)-并发编程基础(上)
  2. java并发编程(2)-并发编程基础(下)
  3. java并发编程(3)-ThreadLocal原理剖析

本篇主要内容是ThreadLocalRandom的原理剖析。ThreadLocalRandom是JDK7以后新增的随机数生成器。在生成随机的场景中,可能很多小伙伴很少用过这个类,甚至没用过,大部分用的还是Random为主。本篇我们先稍微聊聊Random,之后再来理解为什么有了Random了,还要再新增ThreadLocalRandom。

Random

Random是使用比较广泛的随机数生成类,可以说绝大部分的java程序员只要有接触过随机数生成场景都多多少少用它。
Random在生成随机数时,需要一个种子。可以在Random的构造方法中指定种子,如果不指定也没关系,它会有个默认种子。

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

public int nextInt(int bound) {
    if (bound <= 0)
        throw new IllegalArgumentException(BadBound);

    // 根据旧种子生成新种子
    int r = next(31);
    // 根据新种子生成随机数
    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;
}

从Random中的nextInt可以看出内部生成随机数的逻辑就是根据旧种子生成新种子,然后根据新种子生成随机数。

只要你看过我们java并发编程系列前几章的内容,或者对并发有稍微基础,就会知道,在并发环境很有可能会出现多个线程拿到了同一个旧种子,这就导致生产的新种子也是一样的,那自然根据新种子生成的随机数也是一样的。

这种情况其实并不是我们想要的,所以Random使用了AtomicLong来存储种子,AtomicLong是一个基于CAS的原子性操作类,他使我们可以在多线程环境中安全的对Long类型数据读写操作。

与synchronized不同,next方法中的锁是典型的自旋锁,通过CAS操作去竞争锁,如果CAS操作成功了就相当于竞争到锁。比之synchronized,虽然自旋锁不会在加锁释放锁时发生上下文切换,但是会在多线程竞争激烈或者线程长时间持有锁的环境中,导致其他线程产生大量自旋重试,从而引起CPU利用率升高,浪费CPU资源。关于CAS,大家暂时不用太过关注,后面篇章会着重去说。

为了弥补多线程下Random的缺陷,ThreadLocalRandom就此产生。

ThreadLocalRandom

ThreadLocalRandom current = ThreadLocalRandom.current();
long nextLong = current.nextLong(30);

ThreadLocalRandom用起来也比较简单,只需要通过调用current方法获取ThreadLocalRandom对象就可以开始使用它来生成随机数。

ThreadLocalRandom和ThreadLocal是一个原理,都是每个线程维护自己的私有变量(种子),后续的读写操作实际操作的都是自己本地工作内存中的数据。因为使用的种子是自己本地工作内存中的,所以也就不会有Random中的自旋锁的性能问题。

如果对ThreadLocal还没怎么了解过,建议大家可以看一下我们java并发编程系列的第三章ThreadLocal部分的内容,地址也在文章开头贴出来了。将ThreadLocal原理理解透彻对学习ThreadLocalRandom也大有帮助。

接下来我们通过ThreadLocalRandom几个常用的方法来深入的看看它具体的实现原理。

current

private static final sun.misc.Unsafe UNSAFE;
private static final long SEED;
private static final long PROBE;
private static final long SECONDARY;

static final ThreadLocalRandom instance = new ThreadLocalRandom();

static {
    try {
        UNSAFE = sun.misc.Unsafe.getUnsafe();
        Class<?> tk = Thread.class;
        SEED = UNSAFE.objectFieldOffset
            (tk.getDeclaredField("threadLocalRandomSeed"));
        // 是否为第一次调用current(),默认为0
        PROBE = UNSAFE.objectFieldOffset
            (tk.getDeclaredField("threadLocalRandomProbe"));
        SECONDARY = UNSAFE.objectFieldOffset
            (tk.getDeclaredField("threadLocalRandomSecondarySeed"));
    } catch (Exception e) {
        throw new Error(e);
    }
}

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

首先在ThreadLocalRandom类加载阶段,会执行最上面的静态代码块,这块代码块的内容就是获取Thread中三个变量的偏移量,后续针对他们的修改都是直接操作内存修改的,所以需要提前获取他们的偏移量。

这个写法其实也是一种性能优化,因为threadLocalRandomSeed,threadLocalRandomProbe,threadLocalRandomSecondarySeed并不是public修饰,在JUC包中无法访问。在这里使用直接操作内存的方式,没用反射是为了避免因为使用反射而产生的性能消耗。关于UNSAFE,我们会放到后续篇章中讲,本篇大家只要对它能在脑中有个印象就好。

可以看到current方法先去获取了probe,如果probe为0则调用localInit()初始化种子后再将ThreadLocalRandom对象返回出去,如果probe!=0则直接返回。注意,因为current()以及instance是静态的,所以多线程调用,返回的对象是同一个。

localInit整体逻辑还算简单,计算当前线程的probe,根据seeder计算出初始种子。

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

ThreadLocalRandom的nextInt方法整体逻辑和Random差不多,我们就不过多复述了,直接看nextSeed方法。

从nextSeed方法可以看出,这里的原理和ThreadLocal的差不多,将种子保存到Thread中,来达到多线程隔离,这样我们就不需要同步手段,就可以做到多线程安全。

总结

本篇基于《java并发编程之美》。本篇主要内容是Random以及ThreadLocalRandom的原理剖析。为了本篇不过于冗长,一些原本应该放到本篇的知识点打算后续开一章讲。