文章目录
📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌
📙 作者: 编程技术圈(哇哥面试陪跑)
👉 欢迎关注、分享、评论
✔️ 持续分享更多干货内容
🌐🌏🌎➕tcmeta, 欢迎沟通交流
📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌
零、引入
“面试官问我 ThreadLocalRandom 为啥快,我只说‘无竞争’,结果被追问到哑口无言!” 王二垂头丧气地走出面试间,刚面的大厂岗位泡汤了 —— 他知道 ThreadLocalRandom 好用,却讲不清底层原理。哇哥嚼着口香糖过来:“你这是只知其然不知其所以然,ThreadLocalRandom 的底层和 ThreadLocal 有关,但又不一样,今天用奶茶店总部发原料的逻辑给你讲透,下次面试直接把面试官说懵!”

点赞 + 关注,跟着哇哥和王二,从底层源码到实战避坑,彻底吃透 ThreadLocalRandom,面试再也不慌!
一、王二的新坑:ThreadLocalRandom 和 ThreadLocal 啥关系?

王二一直以为 ThreadLocalRandom 是 ThreadLocal 的 “小弟”,直到面试官问:“ThreadLocal 用弱引用存值,ThreadLocalRandom 也是吗?” 他直接卡壳。
“这俩就像同名同姓的两个人,没啥血缘关系,” 哇哥打开 JDK 源码,“ThreadLocal 是‘线程本地存储容器’,ThreadLocalRandom 是‘线程本地随机数生成器’,只是都用到了‘线程私有’的思想。”
✔️ 用奶茶店场景讲透核心区别

假设奶茶店每个收银台(线程)需要取号机(随机数生成器)和原料(线程本地数据):
-
ThreadLocal:负责给每个收银台送 “专属原料”(比如每个线程的用户信息),用 “货架(ThreadLocalMap)” 存,弱引用避免内存泄漏;
-
ThreadLocalRandom:负责给每个收银台配 “专属取号机”,取号机直接放在收银台里(Thread 类的字段),不用货架,更轻量。
“简单说,ThreadLocal 是‘快递员’,ThreadLocalRandom 是‘自带设备’,” 哇哥总结,“这就是它俩的核心区别。”
二、扒开 ThreadLocalRandom 的底层:Thread 类藏着 “小秘密”

哇哥带着王二翻 JDK 源码,发现 ThreadLocalRandom 的快,全靠 Thread 类里的两个 “隐藏字段”:
public class Thread implements Runnable {
// 其他字段省略...
// ThreadLocalRandom的种子(每个线程独立的种子)
@sun.misc.Contended("tlr")
long threadLocalRandomSeed;
// ThreadLocalRandom的探针(用来初始化种子的)
@sun.misc.Contended("tlr")
int threadLocalRandomProbe;
// ThreadLocalRandom的二级种子(优化性能用)
@sun.misc.Contended("tlr")
int threadLocalRandomSecondarySeed;
// 其他方法省略...
}

哇哥解释:“这三个字段就是每个线程的‘取号机核心部件’
- threadLocalRandomSeed 是种子,就像取号机的起始号码
- probe 是探针,用来给新线程分配初始种子
- secondarySeed 是备用种子,优化生成速度。”
📢 ThreadLocalRandom 的核心逻辑:不用 ThreadLocalMap

🔥王二好奇:“为什么不用 ThreadLocal 存这些种子?”
“因为 ThreadLocalMap 有哈希冲突、弱引用这些开销,” 哇哥敲着源码,“ThreadLocalRandom 直接把种子存在 Thread 类里,相当于‘把取号机焊死在收银台上’,不用额外的存储容器,速度自然更快。”
ThreadLocalRandom.current () 底层流程(奶茶店取号机初始化):
// 获取当前线程的ThreadLocalRandom实例
public static ThreadLocalRandom current() {
// 检查当前线程的probe是否为0(没初始化)
if (U.getInt(Thread.currentThread(), PROBE) == 0)
localInit(); // 初始化种子
return instance;
}
@IntrinsicCandidate
public static native Thread currentThread();
当调用ThreadLocalRandom.current()时,底层做了三件事,对应奶茶店给新收银台配取号机:
- 查有没有取号机:获取当前线程,看threadLocalRandomProbe是不是 0(探针为 0 表示没初始化);
- 配初始种子:如果没初始化,调用localInit()方法,给线程分配一个唯一的初始种子(由系统随机数生成,保证每个线程的种子不一样);
- 返回取号机:返回 ThreadLocalRandom 的单例实例(全进程就一个,不用每个线程 new)—— 注意:实例是单例,但种子是每个线程独立的!

👉 简化版本核心源码
public class ThreadLocalRandom extends Random {
// 全局单例实例(所有线程共用一个实例,但种子是线程独立的)
private static final ThreadLocalRandom INSTANCE = new ThreadLocalRandom();
// 禁止new实例
private ThreadLocalRandom() {}
// 获取当前线程的ThreadLocalRandom实例
public static ThreadLocalRandom current() {
// 检查当前线程的probe是否为0(没初始化)
if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0) {
localInit(); // 初始化种子
}
return INSTANCE; // 返回单例实例
}
// 初始化当前线程的线程字段。仅在 Thread.threadLocalRandomProbe 为零时调用,
// 表示需要生成线程本地种子值。注意,尽管初始化纯线程本地,我们仍需依赖(静态)
// 原子生成器来初始化这些值
static final void localInit() {
long seed = RandomSupport.mixMurmur64(seeder.getAndAdd(SEEDER_INCREMENT));
Thread t = Thread.currentThread(), carrier;
U.putLong(t, SEED, seed);
int probe = 0; // if virtual, share probe with carrier
if ((carrier = JLA.currentCarrierThread()) != t &&
(probe = U.getInt(carrier, PROBE)) == 0) {
seed = RandomSupport.mixMurmur64(seeder.getAndAdd(SEEDER_INCREMENT));
U.putLong(carrier, SEED, seed);
}
if (probe == 0 && (probe = probeGenerator.addAndGet(PROBE_INCREMENT)) == 0)
probe = 1; // skip 0
if (carrier != t)
U.putInt(carrier, PROBE, probe);
U.putInt(t, PROBE, probe);
}
// 返回一个伪随机、均匀分布 int 的值,介于0(含)到指定值(独占),
// 取自该随机数生成器的序列。的 nextInt 一般契约是,指定范围内的一个 int
// 值被伪随机生成并返回。所有 bound 可能 int 的值以(大致)相等的概率产生。
public int nextInt(int bound) {
if (bound <= 0)
throw new IllegalArgumentException(BAD_BOUND);
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 { // reject over-represented candidates
for (int u = r;
u - (r = u % bound) + m < 0;
u = next(31))
;
}
return r;
}
// 其他方法省略...
}
❓❓❓ 王二眼睛瞪得像铜铃:“原来 ThreadLocalRandom 是单例?那为什么每个线程的随机数不一样?”

“因为随机数的种子存在 Thread 类里,不是存在实例里,” 哇哥解释,“就像奶茶店的取号机是同一品牌(单例实例),但每个收银台的取号机起始号码不一样(线程独立种子),所以生成的号自然不同。”
三、实战:用 ThreadLocalRandom 实现分布式 ID 生成(可直接抄)

光懂底层不够,哇哥带着王二写了个分布式 ID 生成工具 —— 结合时间戳、机器 ID 和 ThreadLocalRandom,保证并发下唯一且高效,直接能用在生产环境。
📌 分布式 ID 生成器(ThreadLocalRandom 实战)
示例代码:
package cn.tcmeta.randoms;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import static java.lang.Thread.sleep;
/**
* @author: laoren
* @description: 分布式ID生成器:时间戳+机器ID+序列号
* 特点:1. 全局唯一 2. 并发性能高 3. 有序递增(便于数据库索引)
* @version: 1.0.2
*/
public class DistributedIdGenerator {
// 机器ID(部署时从配置文件读取,避免集群环境重复)
private static final long MACHINE_ID;
// 机器ID的位数(最多支持32台机器)
private static final int MACHINE_ID_BITS = 5;
// 机器ID的最大值(最多支持32台机器)
private static final long MAX_MACHINE_ID = (1L << MACHINE_ID_BITS) - 1;
// 序列号的位数(每个机器每个毫秒最多生成32768个ID)
private static final int SEQUENCE_BITS = 15;
// 机器ID的移位(时间戳占44位,机器ID占5位,序列号占15位,总共64位)
private static final int MACHINE_ID_SHIFT = SEQUENCE_BITS;
// 时间戳的移位
private static final int TIMESTAMP_SHIFT = MACHINE_ID_BITS + SEQUENCE_BITS;
// 起始时间戳(2024-01-01 00:00:00,用来减少ID的长度)
private static final long START_TIMESTAMP = 1704067200000L;
// 上一次生成ID的时间戳
private static volatile long lastTimestamp = -1L;
// 当前毫秒内的序列号(全局共享,不用ThreadLocal)
private static final AtomicLong sequence = new AtomicLong(0);
// 最大序列号值
private static final long MAX_SEQUENCE = (1L << SEQUENCE_BITS) - 1;
// 初始化机器ID(这里模拟从配置读取,实际项目中从配置中心获取)
static {
// 实际部署时,替换成当前机器的唯一标识(比如IP的后几位、容器ID等)
MACHINE_ID = getMachineIdFromConfig();
// 校验机器ID是否合法(不能超过31)
if (MACHINE_ID < 0 || MACHINE_ID > MAX_MACHINE_ID) {
throw new IllegalArgumentException("机器ID不合法,范围0-" + MAX_MACHINE_ID);
}
}
// 生成分布式ID(核心方法)
public static synchronized long generateId() {
long timestamp = System.currentTimeMillis() - START_TIMESTAMP;
// 处理时间回拨情况
if (timestamp < lastTimestamp) {
// 等待时钟同步或使用备用方案
timestamp = handleClockBackwards(lastTimestamp);
}
// 如果在同一毫秒内,递增序列号
if (lastTimestamp == timestamp) {
long currentSeq = sequence.incrementAndGet();
// 若序列号超出最大值,阻塞到下一毫秒
if (currentSeq > MAX_SEQUENCE) {
timestamp = tilNextMillis(lastTimestamp);
sequence.set(0);
}
} else {
// 不同毫秒重置序列号
sequence.set(0);
}
lastTimestamp = timestamp;
// 获取当前序列号
long seq = sequence.get();
// 拼接ID:时间戳(44位) << 20 | 机器ID(5位) << 15 | 序列号(15位)
return (timestamp << TIMESTAMP_SHIFT) | (MACHINE_ID << MACHINE_ID_SHIFT) | seq;
}
// 阻塞直到下一个不同的毫秒
private static long tilNextMillis(long lastTimestamp) {
long timestamp;
do {
timestamp = System.currentTimeMillis() - START_TIMESTAMP;
} while (timestamp <= lastTimestamp);
return timestamp;
}
// 处理时钟回拨问题
private static long handleClockBackwards(long lastTimestamp) {
long timestamp;
long diff = lastTimestamp - (System.currentTimeMillis() - START_TIMESTAMP);
// 如果回拨时间较短,等待时钟追上
if (diff <= 10) {
try {
sleep(diff + 1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
timestamp = System.currentTimeMillis() - START_TIMESTAMP;
// 再次检查是否解决了时钟回拨问题
if (timestamp < lastTimestamp) {
throw new RuntimeException("时钟回拨异常,请检查服务器时间同步设置!");
}
} else {
// 回拨时间过长,直接抛出异常
throw new RuntimeException("严重时钟回拨异常,请检查服务器时间同步设置!");
}
return timestamp;
}
// 模拟从配置文件获取机器ID
private static long getMachineIdFromConfig() {
// 实际项目中,这里可以是:
// 1. 读取环境变量(比如K8s的POD_IP后几位)
// 2. 读取配置中心(比如Nacos、Apollo)的配置
// 3. 调用接口获取机器唯一标识
return 1L; // 这里模拟返回机器ID=1
}
// 测试:并发生成ID,验证唯一性和性能
static void main() throws InterruptedException {
// 100个线程并发生成ID
int threadCount = 100;
java.util.concurrent.ExecutorService threadPool = java.util.concurrent.Executors.newFixedThreadPool(threadCount);
java.util.concurrent.CountDownLatch latch = new java.util.concurrent.CountDownLatch(threadCount);
ConcurrentHashMap<Long, Integer> idCounter = new ConcurrentHashMap<>(); // 线程安全的Map用于检测重复
long start = System.currentTimeMillis();
for (int i = 0; i < threadCount; i++) {
threadPool.submit(() -> {
try {
// 每个线程生成1000个ID
for (int j = 0; j < 1000; j++) {
long id = generateId();
idCounter.merge(id, 1, Integer::sum);
}
} finally {
latch.countDown();
}
});
}
latch.await();
long end = System.currentTimeMillis();
threadPool.shutdown();
// 检查是否有重复ID
long duplicateCount = idCounter.values().stream()
.mapToLong(count -> count > 1 ? count - 1 : 0)
.sum();
System.out.println("总耗时:" + (end - start) + "ms");
System.out.println("生成ID总数:" + idCounter.size());
System.out.println("重复ID数量:" + duplicateCount);
if (duplicateCount > 0) {
System.err.println("警告:发现重复ID!");
idCounter.entrySet().stream()
.filter(entry -> entry.getValue() > 1)
.limit(5)
.forEach(entry -> System.err.println("重复ID: " + entry.getKey() + ", 出现次数: " + entry.getValue()));
} else {
System.out.println("恭喜:未发现重复ID!");
}
// 显示部分ID样本(安全访问)
if (!idCounter.isEmpty()) {
System.out.println("第一个ID:" + idCounter.keys().nextElement());
}
}
}
- 程序运行结果:


核心亮点(哇哥划重点)
- 全局唯一性保证
- 采用时间戳+机器ID+序列号的组合方式确保ID全局唯一
- 使用synchronized关键字保证并发安全
- 高性能设计
- 通过序列号机制支持每毫秒生成大量唯一ID(最多32768个)
- 单机并发性能优秀,适合高并发场景
- 有序递增特性
- ID整体趋势递增,有利于数据库索引性能优化
- 便于数据分页和排序操作
技术亮点
- 完善的时钟回拨处理
- 提供两种时钟回拨处理策略:
- 短时间回拨:自动等待时钟同步
- 长时间回拨:抛出异常提醒
- 灵活的配置机制
- 支持动态配置机器ID,适应集群部署
- 可根据实际需求调整位数分配
- 合理的位数分配
- 时间戳(44位)+机器ID(5位)+序列号(15位)的分配兼顾了使用寿命和扩展性
- 支持32台机器部署,每台机器每毫秒生成3万+ID
- 完善的测试验证
- 内置并发测试用例,可验证唯一性和性能
- 提供重复ID检测机制
四、避坑指南:90% 的人踩过的 3 个坑
王二照着代码改的时候,又踩了几个坑,哇哥干脆整理成 “避坑手册”:
❌ 坑 1:在单线程里反复调用 current (),以为会生成新实例
// 错误认知:以为每次current()都会生成新实例
for (int i = 0; i < 10; i++) {
ThreadLocalRandom random = ThreadLocalRandom.current();
System.out.println(random == ThreadLocalRandom.current()); // 输出true,是同一个实例
}
原因:ThreadLocalRandom 是单例,current () 只是返回全局实例,不会新建。
❌ 坑 2:用 ThreadLocalRandom 生成加密随机数(比如验证码)
// 错误用法:生成验证码(有安全风险)
String code = String.valueOf(ThreadLocalRandom.current().nextInt(10000));
原因:ThreadLocalRandom 的随机数是 “伪随机”,可预测,容易被破解;生成验证码、支付密码等,必须用SecureRandom:
// 正确用法:SecureRandom生成加密随机数
SecureRandom secureRandom = new SecureRandom();
String code = String.valueOf(secureRandom.nextInt(10000));
❌ 坑 3:在子线程里使用父线程的 ThreadLocalRandom 种子
// 错误场景:父线程生成种子,子线程直接用,可能导致种子重复
Thread parentThread = new Thread(() -> {
ThreadLocalRandom.current().nextInt(); // 父线程初始化种子
new Thread(() -> {
// 子线程没初始化自己的种子,可能复用父线程的种子片段
System.out.println(ThreadLocalRandom.current().nextInt());
}).start();
});

原因: 子线程会继承父线程的 Thread 类字段,但 ThreadLocalRandom 会在子线程第一次调用 current () 时重新初始化种子,所以只要调用 current (),就不会有问题 —— 避免直接操作 Thread 类的 seed 字段即可。
五、总结:ThreadLocalRandom 底层核心(王二记满小本本)
✅ 总结
- 底层依赖 Thread 类字段:种子存在 Thread 的 threadLocalRandomSeed 里,不用 ThreadLocalMap,更轻量;
- 单例实例 + 线程独立种子:实例是全局单例,但种子每个线程独立,所以随机数不重复;
- 初始化靠探针:probe 字段为 0 时触发初始化,分配唯一种子;
- 并发性能之王:无锁竞争,生成速度比 Random 快 10 倍以上,是并发场景的首选。
🔥 哇哥的面试技巧
“面试时被问底层,你就按这个逻辑说,” 哇哥传授技巧,“先讲‘奶茶店取号机’的比喻,再讲 Thread 类的 seed 字段,然后说单例实例 + 线程独立种子的设计,最后手写分布式 ID 生成代码 —— 这套组合拳打出去,面试官绝对觉得你是并发高手!”

关注我,下次咱们扒一扒 ThreadLocalRandom 和 SecureRandom 的区别 —— 为什么 SecureRandom 更安全?性能差多少?什么场景该用哪个?让你不仅会写高性能代码,还能兼顾安全,成为全能的 Java 工程师!


被折叠的 条评论
为什么被折叠?



