Java多线程进阶(四六)—— J.U.C之补遗:ThreadLocalRandom
一、ThreadLocalRandom简介
ThreadLocalRandom
是在JDK1.7之后引入的一个类,为Random
的一个子类,在多线程环境中相较于Random
来说它会有更小的开销和争用。
When applicable, use of
ThreadLocalRandom
rather than sharedRandom
objects in concurrent programs will typically encounter much less overhead and contention.
二、ThreadLocalRandom的原理
Random的介绍
Random
的部分定义如下:
public class Random implements java.io.Serializable {
private final AtomicLong seed;
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)); // 若CAS置新值失败则继续循环
return (int)(nextseed >>> (48 - bits));
}
...
}
每一次生成随机数的时候,都会进行CAS操作重置seed,在多个线程使用同一个Random
对象时若遇到冲突就会导致线程频繁进行自旋,性能因此受到影响。而为了解决这个问题可以考虑不同的线程有不同的seed,ThreadLocalRandom
籍此而生。
ThreadLocalRandom代码剖析
ThreadLocalRandom
在解决Random
问题的思路上主要是不同的线程使用不同的seed,再根据旧seed生成新seed,线程间seed互不影响,顺着这个思路,JDK团队对在1.7和1.8用了不同的方式来实现同样的目的。以JDK1.8为例,Thread
类里面添加了和Random
相关的字段:
public class Thread implements Runnable {
/**
* ThreadLocalRandom的当前seed
*/
@sun.misc.Contended("tlr") // @sun.misc.Contended为了避免伪共享
long threadLocalRandomSeed;
/**
* Probe hash 值; 如果初始化了则为非0
*/
@sun.misc.Contended("tlr")
int threadLocalRandomProbe;
/**
* 从ThreadLocalRandom公共序列里面分离出来的辅助seed
*/
@sun.misc.Contended("tlr")
int threadLocalRandomSecondarySeed;
}
下面的代码让ThreadLocalRandom
和上面的Thread
属性关联起来:
public class ThreadLocalRandom extends Random {
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;
SEED = UNSAFE.objectFieldOffset
(tk.getDeclaredField("threadLocalRandomSeed"));
PROBE = UNSAFE.objectFieldOffset
(tk.getDeclaredField("threadLocalRandomProbe"));
SECONDARY = UNSAFE.objectFieldOffset
(tk.getDeclaredField("threadLocalRandomSecondarySeed"));
} catch (Exception e) {
throw new Error(e);
}
}
}
继续拜读ThreadLocalRandom
的代码定义:
public class ThreadLocalRandom extends Random {
/**
* 定义为私有,让外部无法直接调用构造方法初始化对象
*/
private ThreadLocalRandom() {
initialized = true;
}
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);
}
/**
* 定义为static final类型,所有使用ThreadLocalRandom.current()方法的时候得到的是同一个对象
*/
static final ThreadLocalRandom instance = new ThreadLocalRandom();
public static ThreadLocalRandom current() {
if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
localInit();
return instance;
}
private static final AtomicLong seeder = new AtomicLong(initialSeed());
/**
* 设置初始的seed
*/
private static long initialSeed() {
String sec = VM.getSavedProperty("java.util.secureRandomSeed"); // 可以通过设置参数来让初始的seed不被预测
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()));
}
...
}
从代码定义中我们看到,seed已经不再是AtomicLong
类型,而直接是在Thread
里面定义的long型,再不用担心不同线程在seed上进行CAS操作了。
ThreadLocalRandom
的current
方法在开始时判断当前线程是否已经被初始化过,如果没有,则进行seed值的初始化,同时probe也会被置为非0值,之后同一线程再次调用ThreadLocalRandom.current()
时发现probe非0则直接返回instance,而其他线程初次进入的时候,线程的probe仍然是0,则继续初始化。
public int nextInt() {
return mix32(nextSeed());
}
final long nextSeed() {
Thread t; long r; // 读取并更新每个线程的seed
UNSAFE.putLong(t = Thread.currentThread(), SEED,
r = UNSAFE.getLong(t, SEED) + GAMMA);
return r;
}
通过nextInt
的代码我们可以看到,每次执行获取随机数时,都是对本线程的数据进行操作,所以线程间的seed互不影响,自然没有Random
中存在的问题。
三、总结
ThreadLocalRandom
通过巧妙的设计,将最初Random
共享而影响性能的seed挪到了Thread
本身,让各个Thread
有独立的seed来进行随机数的处理,避免了多线程对同一Random
实例的争用。
在Random
的设计中,其seed可以是被指定的,而在此类中计算随机数的算法是固定的,在具有同等seed的情况下生成的随机序列是确定且一致的,因此ThreadLocalRandom
不支持setSeed
方法,且不允许通过其他方式设置seed,尽量避免随机数序列一样的情况,不过因为ThreadLocalRandom
仍然是伪随机数生成器,即使指定了初始seed是java.util.secureRandomSeed
,若要使用更严格的随机数,请使用SecureRandom
,但是要面对性能损失。
感谢赞赏~