ThreadLocalRandom的种子隔离机制隔离的是每个线程对seed变量的读写路径,即把seed存入Thread对象的threadLocalRandomSeed等字段,通过Unsafe直接操作内存偏移量,实现无共享、无CAS、无自旋的线程本地随机数生成。

ThreadLocalRandom 的种子隔离机制到底隔离了什么
它隔离的不是“随机数”,而是每个线程对 seed 变量的读写路径。Random 的 seed 是一个共享的 AtomicLong 字段,所有线程都通过 CAS 去争抢更新;而 ThreadLocalRandom 把种子直接存进 Thread 对象内部的三个字段:threadLocalRandomSeed、threadLocalRandomProbe、threadLocalRandomSecondarySeed。这些字段由 Unsafe 直接基于内存偏移量操作,完全绕开对象引用和锁机制。
关键点在于:没有共享变量 → 没有 CAS 竞争 → 没有自旋重试 → 不吃 CPU。
为什么 Random 的 CAS 会变成自旋锁竞争
看 Random.next() 源码就知道问题在哪:
do {
oldseed = seed.get();
nextseed = (oldseed * multiplier + addend) & mask;
} while (!seed.compareAndSet(oldseed, nextseed));
当多个线程同时执行这段代码时:
- 线程 A 读到 seed=100,算出 nextseed=201,准备 CAS 更新
- 线程 B 在同一毫秒内也读到 seed=100,算出 nextseed=202,抢先完成 CAS,把 seed 改成 202
- 线程 A 的 compareAndSet 失败,只能重来:再读 seed(现在是 202),再算,再试
线程越多,失败概率越高。在 16 核服务器上,32 线程并发时,Random.nextInt() 的平均 CAS 失败率可达 60% 以上,大量 CPU 时间耗在空转上,不是卡在业务,是卡在“抢骰子”。
ThreadLocalRandom 怎么避免自旋,又不牺牲线程安全
它根本没用“同步”思路,而是用“分治”:每个线程只动自己的种子,连内存地址都不一样。调用 ThreadLocalRandom.current() 时:
- 若当前线程尚未初始化,则用
UNSAFE.putLong(t, SEED, ...)往本线程对象里写入初始种子 - 后续每次
nextInt()都是UNSAFE.getLong(t, SEED)读取 + 计算 +UNSAFE.putLong(t, SEED, newSeed)写回 - 全程无锁、无 CAS、无共享、无等待
注意:@Contended 注解被加在 threadLocalRandomSeed 所在的缓存行上,防止伪共享——否则即使地址不同,CPU 缓存行冲突仍会导致性能抖动。
容易被忽略的初始化陷阱
很多人以为 “用一次 current() 就够了”,于是写成:
private static final ThreadLocalRandom RANDOM = ThreadLocalRandom.current();
这行代码在类加载时执行,只会调用一次 current(),且是在主线程里初始化的。结果所有工作线程拿到的其实是同一个初始实例(或更糟:未正确绑定到各自线程)。真正有效的写法永远是:
int n = ThreadLocalRandom.current().nextInt(1, 101);
每次调用都触发线程本地检查与懒初始化。ForkJoinPool 中尤其要注意:它的 worker thread 可能复用,但 threadLocalRandomSeed 不会自动传播,所以必须确保每次任务中都调用 current(),而不是依赖外部传入的实例。










