ThreadLocalRandom
是 JDK7
里 J.U.C
包的新成员,它是专门设计用于高并发场景下的随机数生成工具。那么ThreadLocalRandom
为了提高并发能力做了哪些方面的工作呢?
- 资源隔离,不采用串行化的方式
- 性能提升
- 缓存行伪共享
- 种子数据粗粒度化
实现思路
资源隔离
之前有比较过 synchronized
和 ThreadLocal
的异同,前者侧重资源访问串行化,后者侧重线程级资源独立化。
我们先看看
Random
处理并发的能力: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;
// CAS进行交换
} while (!seed.compareAndSet(oldseed, nextseed));
return (int)(nextseed >>> (48 - bits));
}
我们可以看到
Random
主要采用CAS
保证资源同一时间只能被一个线程访问,但若在高并发的场景下,长时间的CAS失败自旋会占用大量的CPU
资源,这也是为什么出现了ThreadLocalRandom
的原因。接下来看看
ThreadLocalRandom
的思路: ```java /**- 当前线程初始化 随机数数据 */ static final void localInit() { // 生成probe计数器 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(); // 将种子和probe数据放入 线程中 UNSAFE.putLong(t, SEED, seed); UNSAFE.putInt(t, PROBE, probe); }
/**
- 我们通过 current() 方法获取当前线程的ThreadLocalRandom
*/
public static ThreadLocalRandom current() {
// 如果一个线程尚未初始化
if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
return instance; } ```// 则执行localInit()初始化当前线程的数据
localInit();
里面关于 probe、seeder会在后续进行分析
两者在隔离方式上采用了截然不同的方式,那么相应的,应用场景也会有所不同: Random
适用的范围更加广泛,它的粒度更细,可以一个 Random()
全局使用,但是相应的并发能力就会低一些; ThreadLocalRandom
的作用范围通常只在线程内,并发能力高一些。
工作流程
ThreadLocalRandom
结构关系图如下所示:
每个线程都通过 ThreadLocalRandom.current()
初始化当前线程的数据并获得相同的 ThreadLocalRandom
// 这里是 Thread1
public void thread1(){
ThreadLocalRandom random = ThreadLocalRandom.current();
}
// 这里是 Thread2
public void thread2(){
ThreadLocalRandom random = ThreadLocalRandom.current();
}
// 注意,上面获取到的 ThreadLocalRandom 都是相同的对象!
//------------------------------下面是 ThreadLocalRandom 的初始化 ----------------------
static final ThreadLocalRandom instance = new ThreadLocalRandom();
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);
}
public static ThreadLocalRandom current() {
if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
localInit();
return instance;
}
简单来说, ThreadLocalRandom
就是一个工具对象,全局仅这一个;它将数据绑定在当前线程上,每个线程就通过使用 ThreadLocalRandom
实现各自线程的随机数生成。这里可能会对这种设计方式有疑问,为什么不直接上 ThreadLocal
呢?
ThreadLocal
如果不手动remove()
仍然有概率发生内存泄漏,作为一个工具,不可能再让用户再调什么类似destroy()
的方法,麻烦、不友好- 如何设计一个对象无论怎么样调用,一个线程只用同一个对象,不同线程使用不同对象?我想这一定需要
Thread
配合,因为ThreadLocal
你要么是static
,全局仅一个;要么就是每次new
的时候都新建一个ThreadLocal
我觉得这种设计方式挺独特的,当每个线程只需要一个对象,又想保证线程级隔离,就与 Thread
绑定关系即可。
性能改进
缓存行伪共享消除
前面讲到 ThreadLocalRandom
都是通过获取各自线程的数据从而实现线程间隔离,我们看看官方在 Thread
里面下了什么药:
/** The current seed for a ThreadLocalRandom */
@sun.misc.Contended("tlr")
long threadLocalRandomSeed;
/** Probe hash value; nonzero if threadLocalRandomSeed initialized */
@sun.misc.Contended("tlr")
int threadLocalRandomProbe;
/** Secondary seed isolated from public ThreadLocalRandom sequence */
@sun.misc.Contended("tlr")
int threadLocalRandomSecondarySeed;
threadLocalRandomSeed
,ThreadLocalNumber
使用的随机数种子,每个线程持有一份随机数种子从而达到线程间隔离threadLocalRandomProbe
,探针,用来判断当前线程是否已经初始化过随机数threadLocalRandomSecondarySeed
,用来与threadLocalRandomSeed
隔离,这货主要被ConcurrentSkipListMap
、ForkJoinPool
等其他并发工具使用;隔离开来的原因是:“避免干扰用户级随机数;其次为了减少线程间的干扰,现在只有用自己线程里的随机数即可”。threadLocalRandomSecondarySeed 可以参考
ConcurrentSkipListMap
的注释,搜索secondary seed
关键字即可找到对应的说明
而这里比较强的点是这个 @sun.misc.Contended("tlr")
,这货可以消除缓存行伪共享。因为 CPU
操作缓存都是针对一个缓存行的,而一个缓存行里可包含多个数据;如果不同的线程的数据都在一个缓存行中,在高并发情况下,就会对缓存行不断进行清理、更新,所以使用该注解可以保证一个缓存行只有一个数据。具体的缓存行伪共享可以参考这篇文章。
Unsafe代替反射
因为 Thread
和 TheadLocalRandom
所在包不一样,而 Thread
里的数据的可见级别又不能是 public
,所以需要一种手段来让 ThreadLocalRandom
获取到 Thread
里面的数据。使用反射?反射性能过于低下,所以打算直接用Unsafe
操作内存来获取数据。
扩展
探针threadLocalRandomProbe的作用
用来特殊标记一个线程,通过 probe
可以获取到当前线程的“身份证”~在 ThreadLocalRandom
中基本就是拿来判断是否初始化过了;其他的并发工具可能会用做他用。