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 中基本就是拿来判断是否初始化过了;其他的并发工具可能会用做他用。
