java并发编程系列前文
本篇主要内容是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的原理剖析。为了本篇不过于冗长,一些原本应该放到本篇的知识点打算后续开一章讲。