【036】面试被问 ThreadLocalRandom 底层?用奶茶店排队讲透!


在这里插入图片描述
📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌

📙 作者: 编程技术圈(哇哥面试陪跑)
👉 欢迎关注、分享、评论
✔️ 持续分享更多干货内容
🌐🌏🌎➕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 工程师!

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值