编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

  • Cpp入门到精通

  • Java入门精通

    • README
    • 入门教程

    • 综合案例

    • 专栏博客

      • README
      • JVM内存模型与对象
      • 类加载与双亲委派
      • 垃圾回收与GC调优
      • 异常体系与JVM机制
      • 字节码指令集javap实战
      • JIT编译与去优化机制
      • JVM性能诊断工具链
      • OOM八大现场全景剖析
      • JVM参数调优全景图
      • GraalVM与AOT编译原理
      • HashMap底层哈希设计
      • String不可变与常量池
      • ArrayList与LinkedList源码
      • ConcurrentHashMap并发
      • TreeMap与红黑树原理
      • LinkedHashMap与LRU实现
      • Java数字类型原理
      • Object通用方法的契约
      • 泛型擦除与类型系统
      • 枚举原理与最佳实践
      • 注解原理与编译期处理
      • Lambda与引用底层原理
      • Stream原理与流水线设计
      • Optional设计原理
      • Record密封类与模式
      • 反射机制与动态代理
      • MethodHandle与VarHandle
      • 三大字节码框架对比
      • JavaAgent与Instrumentation机制
      • AOP三种实现路线对比
      • synchronized与锁升级
      • volatile与JMM内存模型
      • 线程池核心源码设计
      • Thread线程生命周期
        • 1. 案例引入
          • 1.1 ThreadLocal 引发的线上 OOM
          • 1.2 父子线程拿不到 MDC 的诡异 bug
          • 1.3 我们要回答什么
        • 2. Thread 整体架构
          • 2.1 Thread 类的字段全貌
          • 2.2 Java Thread 与 OS Thread 的 1:1 映射
          • 2.3 六态状态机
        • 3. start 与 run 源码
          • 3.1 start() 的一次性约束
          • 3.2 start0 native 实现链路
          • 3.3 直接调用 run() 的常见错误
        • 4. join/sleep/yield 与协作
          • 4.1 join 基于 wait 的等待协议
          • 4.2 sleep 与 wait 的本质差别
          • 4.3 yield 的"建议性"语义
        • 5. interrupt 三件套
          • 5.1 中断标志位的真相
          • 5.2 InterruptedException 何时抛
          • 5.3 isInterrupted vs interrupted 的陷阱
          • 5.4 stop/suspend/resume 为什么被弃用
        • 6. ThreadLocal 源码与设计
          • 6.1 数据结构:ThreadLocalMap 在 Thread 上
          • 6.2 Entry 的 WeakReference 设计
          • 6.3 开放寻址 + 黄金分割哈希
          • 6.4 set/get/remove 源码追踪
        • 7. ThreadLocalMap 内存泄漏真凶
          • 7.1 Key 弱引用 + Value 强引用的非对称
          • 7.2 线程池场景下的链式泄漏
          • 7.3 五种规避方案
        • 8. InheritableThreadLocal 与 TTL
          • 8.1 父子线程传递机制
          • 8.2 线程池场景为什么失效
          • 8.3 阿里 TransmittableThreadLocal
        • 9. 综合回扣与卷五开篇
          • 9.1 案例真相揭晓
          • 9.2 设计哲学回扣
          • 9.3 卷五十篇知识地图
          • 9.4 速查表
      • AQS同步框架源码
      • 并发锁三剑客
      • CAS和Atomic深入分析
      • 五大同步器对比
      • CompletableFuture异步
      • IO模型演进BIO到AIO
      • ByteBuffer与堆外内存
      • 序列化原理与替代方案
      • 文件IO与NIO.2
      • 面向对象的真意
      • JDK设计模式上
      • JDK设计模式下
      • SPI与模块化设计
  • Go入门到精通

  • JavaScript入门

  • CodeX
  • Java入门精通
  • 专栏博客
杨充
2026-06-02
目录

Thread线程生命周期

# 34.Thread线程生命周期

# 目录介绍

  • 1. 案例引入
    • 1.1 ThreadLocal 引发的线上 OOM
    • 1.2 父子线程拿不到 MDC 的诡异 bug
    • 1.3 我们要回答什么
  • 2. Thread 整体架构
    • 2.1 Thread 类的字段全貌
    • 2.2 Java Thread 与 OS Thread 的 1:1 映射
    • 2.3 六态状态机
  • 3. start 与 run 源码
    • 3.1 start() 的一次性约束
    • 3.2 start0 native 实现链路
    • 3.3 直接调用 run() 的常见错误
  • 4. join/sleep/yield 与协作
    • 4.1 join 基于 wait 的等待协议
    • 4.2 sleep 与 wait 的本质差别
    • 4.3 yield 的"建议性"语义
  • 5. interrupt 三件套
    • 5.1 中断标志位的真相
    • 5.2 InterruptedException 何时抛
    • 5.3 isInterrupted vs interrupted 的陷阱
    • 5.4 stop/suspend/resume 为什么被弃用
  • 6. ThreadLocal 源码与设计
    • 6.1 数据结构:ThreadLocalMap 在 Thread 上
    • 6.2 Entry 的 WeakReference 设计
    • 6.3 开放寻址 + 黄金分割哈希
    • 6.4 set/get/remove 源码追踪
  • 7. ThreadLocalMap 内存泄漏真凶
    • 7.1 Key 弱引用 + Value 强引用的非对称
    • 7.2 线程池场景下的链式泄漏
    • 7.3 五种规避方案
  • 8. InheritableThreadLocal 与 TTL
    • 8.1 父子线程传递机制
    • 8.2 线程池场景为什么失效
    • 8.3 阿里 TransmittableThreadLocal
  • 9. 综合回扣与卷五开篇
    • 9.1 案例真相揭晓
    • 9.2 设计哲学回扣
    • 9.3 卷五十篇知识地图
    • 9.4 速查表

# 1. 案例引入

# 1.1 ThreadLocal 引发的线上 OOM

某次大促压测——一个看似无害的工具类把生产 JVM 老年代撑爆:

public class UserContext {
    private static final ThreadLocal<UserInfo> CTX = new ThreadLocal<>();

    public static void set(UserInfo u) { CTX.set(u); }
    public static UserInfo get()       { return CTX.get(); }
    // ★ 没有 remove()!
}

@RestController
public class OrderController {
    @PostMapping("/order")
    public Result placeOrder(@RequestBody OrderReq req) {
        UserContext.set(parseUser(req));      // 每个请求都 set
        return orderService.placeOrder(req);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

故障现象:

  • 系统跑 6 小时后老年代占用 90%
  • Tomcat 线程池 200 个线程,但 ThreadLocalMap 里堆了百万级 Entry
  • UserInfo 持有 Map<String, Object> attributes,体积 ≈ 10KB → 直接 2GB 老年代
  • 重启就好,但每天定时复发

疑惑:

  • ThreadLocal 不是设计用来"自动隔离"的吗?为什么会泄漏?
  • 网上都说 Entry 用了 WeakReference,应该自动 GC 才对,为什么不行?
  • 加一行 remove() 真能根治?

# 1.2 父子线程拿不到 MDC 的诡异 bug

另一个项目用 SLF4J MDC 打链路 ID:

@RestController
public class TraceController {
    @GetMapping("/query")
    public Result query() {
        MDC.put("traceId", UUID.randomUUID().toString());
        log.info("收到请求");                    // ✅ 日志有 traceId

        CompletableFuture.runAsync(() -> {
            log.info("异步任务执行");             // ❌ 日志里 traceId 丢了!
        });
        return Result.ok();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

疑惑:

  • MDC 内部就是 ThreadLocal,为什么父线程 set 的值在子线程里读不到?
  • 用 InheritableThreadLocal 改一下能解决吗?
  • 为什么阿里的 TransmittableThreadLocal 必须配合特定 Executor 才行?

# 1.3 我们要回答什么

第 35 篇是卷五《并发编程深水区》第 4 篇,承接已发布的 08(synchronized)、20(HashMap 多线程)、24(CompletableFuture),开启 36~44 共 7 篇并发硬核内容的"地基铺设":

卷五十篇知识递进:
   08 篇  →  synchronized + 锁升级(已写)
   20 篇  →  HashMap 多线程死链(已写)
   24 篇  →  CompletableFuture(已写)
   35 篇  →  Thread 与线程生命周期 ← 本篇(地基)
   36 篇  →  AQS(地基之上的同步框架)
   37 篇  →  ReentrantLock & ReadWriteLock & StampedLock
   38 篇  →  CAS & Atomic & Unsafe
   39 篇  →  线程池 ThreadPoolExecutor
   40 篇  →  Loom 虚拟线程基础
   41 篇  →  Loom 虚拟线程进阶(含 Pinning 问题)
1
2
3
4
5
6
7
8
9
10
11

带着 5 个核心问题展开:

追问 ①:Thread 状态机 6 态如何流转?哪些 API 触发哪些跃迁?  → §2、§4
追问 ②:start() 为什么不能调用两次?run() 直接调用会怎样?   → §3
追问 ③:interrupt 不是真正终止线程,那它做了什么?           → §5
追问 ④:ThreadLocal Entry 的弱引用为什么仍会泄漏?           → §7
追问 ⑤:跨线程上下文传递为什么必须用 TTL?                  → §8
1
2
3
4
5

# 2. Thread 整体架构

# 2.1 Thread 类的字段全貌

java.lang.Thread 在 JDK 21 中的关键字段:

public class Thread implements Runnable {
    private volatile String name;                    // 线程名
    private int            priority;                 // 优先级 1-10
    private boolean        daemon = false;           // 守护线程标志
    private long           tid;                      // 线程 ID(JVM 内唯一)
    private volatile int   threadStatus;             // ★ 状态码(对应 6 态)
    
    Runnable               target;                   // ★ 真正执行的任务
    private ClassLoader    contextClassLoader;       // ★ 上下文 ClassLoader(卷一第 5 篇钩子)
    
    ThreadLocal.ThreadLocalMap threadLocals;         // ★ ThreadLocal 容器
    ThreadLocal.ThreadLocalMap inheritableThreadLocals; // ★ 可继承 ThreadLocal 容器
    
    private volatile Interruptible nioBlocker;       // 中断时唤醒 NIO
    
    // JDK 21 虚拟线程相关(41 篇详解)
    private final Continuation cont;                 // 协程载体
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

关键观察:

  • threadLocals 和 inheritableThreadLocals 是 Thread 实例字段——这就解释了为什么 ThreadLocal 能做线程隔离(数据物理上挂在 Thread 对象里)
  • target 字段持有 Runnable——这是 new Thread(runnable) 与 run() 的桥梁

# 2.2 Java Thread 与 OS Thread 的 1:1 映射

        Java 应用层
   ┌──────────────────────┐
   │  java.lang.Thread t  │   ← 用户视角的 "线程对象"
   └──────────┬───────────┘
              │ 持有
              ↓
   ┌──────────────────────┐
   │  JVM 层 JavaThread   │   ← HotSpot C++ 对象
   └──────────┬───────────┘
              │ 1:1 绑定
              ↓
   ┌──────────────────────┐
   │  OS 层 pthread_t     │   ← Linux NPTL 内核线程
   └──────────────────────┘
              │ 1:1 调度
              ↓
   ┌──────────────────────┐
   │  CPU 核心             │
   └──────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

这个 1:1 映射是平台线程的核心瓶颈,也正是 JDK 21 虚拟线程(卷五 41 篇)要打破的——M:N 调度,N 个虚拟线程复用 M 个 OS 线程。

# 2.3 六态状态机

public enum State {
    NEW,            // 新建,尚未 start
    RUNNABLE,       // 可运行(Java 不区分 ready 和 running)
    BLOCKED,        // 阻塞,等 monitor 锁
    WAITING,        // 无限等待
    TIMED_WAITING,  // 限时等待
    TERMINATED      // 终止
}
1
2
3
4
5
6
7
8

完整迁移图(理解了这张图,并发面试就过了一半):

                    ┌────────┐
                    │  NEW   │
                    └───┬────┘
                        │ start()
                        ↓
   ┌──────────────► RUNNABLE ◄──────────────┐
   │                    │                    │
   │  notify/notifyAll  │                    │  unpark
   │  signal/signalAll  │                    │
   │  时间到            ├──────────┐         │
   │                    │          │         │
   │ synchronized       │ wait()   │ park()  │
   │ 抢不到锁           │ join()   │         │
   │                    │ sleep(t) │         │
   │                    │          │         │
   ↓                    ↓          ↓         │
┌──────┐         ┌─────────┐ ┌──────────────┐│
│BLOCK │         │ WAITING │ │TIMED_WAITING ││
└──┬───┘         └────┬────┘ └──────┬───────┘│
   │                  │             │        │
   │ 抢到锁           └─────────────┴────────┘
   └──────────► RUNNABLE
                        │ run() 结束 / 异常
                        ↓
                  ┌────────────┐
                  │ TERMINATED │
                  └────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

易错点:

  • ⚠️ BLOCKED 仅指等 monitor 锁(synchronized);等 ReentrantLock 是 WAITING
  • ⚠️ Java 的 RUNNABLE 包含 OS 层的 ready 与 running 两态——这是 JVM 故意做的简化
  • ⚠️ wait() 与 wait(timeout) 分别进入 WAITING 与 TIMED_WAITING,状态码不同

# 3. start 与 run 源码

# 3.1 start() 的一次性约束

// java.lang.Thread (JDK 21 简化)
public synchronized void start() {
    if (holder.threadStatus != 0)               // ★ 不为 0 表示已 start 过
        throw new IllegalThreadStateException();
    
    group.add(this);                             // 加入线程组
    boolean started = false;
    try {
        start0();                                // ★ native 入口
        started = true;
    } finally {
        if (!started) group.threadStartFailed(this);
    }
}

private native void start0();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

为什么只能 start 一次:threadStatus 初值 0,进入 RUNNABLE 后变为非 0,再次调用立刻抛 IllegalThreadStateException——这是 JVM 强制约束。甚至连终止后的线程都不能重启——因为 OS 层的 pthread 已经销毁。

# 3.2 start0 native 实现链路

Thread.start0()  [Java]
       ↓
JVM_StartThread (jvm.cpp)
       ↓
new JavaThread(...)            ← 创建 JVM 层线程对象
       ↓
os::create_thread(...)         ← 调 OS API
       ↓
pthread_create(..., thread_entry, ...)  ← Linux 系统调用
       ↓
新内核线程被调度
       ↓
执行 thread_entry()
       ↓
JavaCalls::call_virtual(this.run)   ← 反向调 Java 层 run()
       ↓
Runnable.run() 业务代码执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

关键洞察:JVM 创建线程要走完整的 OS 系统调用 + 用户态/内核态切换 + 栈空间分配(默认 1MB)——这就是为什么平台线程"贵",单机几千个就到上限。虚拟线程则跳过这条链路(41 篇详解)。

# 3.3 直接调用 run() 的常见错误

Thread t = new Thread(() -> System.out.println(Thread.currentThread().getName()));

t.start();   // 输出 Thread-0      ← ✅ 启动了新线程
t.run();     // 输出 main          ← ❌ 在主线程里执行了 run,没启新线程
1
2
3
4

根因:run() 是普通方法,调用它跟调用 t.toString() 没区别——只有 start() 走 native 链路才会创建 OS 线程。这是面试出现率 90% 的题,但仍是新手最爱踩的坑。

# 4. join/sleep/yield 与协作

# 4.1 join 基于 wait 的等待协议

// JDK 21 简化
public final synchronized void join(long millis) throws InterruptedException {
    if (millis < 0) throw new IllegalArgumentException();
    if (millis == 0) {
        while (isAlive()) wait(0);              // ★ 永久等
    } else {
        long startTime = System.nanoTime();
        long delay = millis;
        while (isAlive()) {
            if (delay <= 0) break;
            wait(delay);
            delay = millis - NANOSECONDS.toMillis(System.nanoTime() - startTime);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

惊人发现:join 的本质是调用方对目标线程对象 wait——t.join() 等价于 synchronized(t) { while(t.isAlive()) t.wait(); }。

那么谁来 notify?答案在 JVM 内部:当目标线程退出时,JVM 自动对 Thread 对象做 notifyAll。这就是为什么永远不要在业务代码里对 Thread 对象 wait/notify——会和 JVM 内置机制冲突。

# 4.2 sleep 与 wait 的本质差别

维度 Thread.sleep(t) obj.wait(t)
归属 Thread 静态方法 Object 实例方法
是否释放锁 ❌ 不释放 ✅ 释放
唤醒方式 时间到自动 notify/notifyAll/时间到
状态 TIMED_WAITING WAITING / TIMED_WAITING
调用前提 任意位置 必须持有该对象 monitor

最经典的坑:

synchronized(lock) {
    Thread.sleep(5000);     // ★ 5 秒内 lock 一直被这个线程占着,其他线程全部 BLOCKED
}

synchronized(lock) {
    lock.wait(5000);        // ★ 立刻释放 lock,5 秒后或被 notify 时重新竞争
}
1
2
3
4
5
6
7

# 4.3 yield 的"建议性"语义

public static native void yield();
1

yield() 只是给 OS 调度器一个提示——"我可以让出 CPU"——OS 完全可以无视。生产中几乎用不到,唯一场景是测试代码里制造调度抖动来复现并发 bug。

# 5. interrupt 三件套

# 5.1 中断标志位的真相

Java 没有真正"杀死线程"的 API——interrupt() 只是设置一个 boolean 标志位:

public void interrupt() {
    if (this != Thread.currentThread()) checkAccess();
    synchronized (interruptLock) {
        Interruptible b = nioBlocker;
        if (b != null) {
            interrupted = true;
            interrupt0();           // ★ 仅设置标志位,并 unpark 线程
            b.interrupt(this);      // 唤醒 NIO 阻塞
            return;
        }
    }
    interrupted = true;
    interrupt0();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

这意味着:被中断的线程继续运行——除非线程自己主动检查标志位或正在响应中断的方法里。

# 5.2 InterruptedException 何时抛

只有这些方法会响应中断(抛 InterruptedException + 清除标志位):

Object.wait() / wait(timeout)
Thread.sleep(timeout)
Thread.join() / join(timeout)
LockSupport.park() / parkNanos() / parkUntil()
BlockingQueue.take() / put()
CountDownLatch.await()
Semaphore.acquire()
Condition.await()
1
2
3
4
5
6
7
8

响应中断的标准模板:

while (!Thread.currentThread().isInterrupted()) {
    try {
        // 执行任务
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();   // ★ 关键:恢复中断状态
        break;                                 // 优雅退出
    }
}
1
2
3
4
5
6
7
8
9

为什么 catch 后还要 interrupt():因为抛 InterruptedException 时 JVM 已自动清除标志位——如果上层代码依赖该标志判断终止,必须手动恢复。

# 5.3 isInterrupted vs interrupted 的陷阱

public boolean isInterrupted()              // 实例方法,不清除标志
public static boolean interrupted()         // 静态方法,★ 检查并清除标志
1
2

踩坑示例:

Thread.currentThread().interrupt();
System.out.println(Thread.interrupted());           // true
System.out.println(Thread.interrupted());           // false ← ★ 已被清除
System.out.println(Thread.currentThread().isInterrupted());  // false
1
2
3
4

记忆口诀:带 is 的不清除,不带 is 的清除。

# 5.4 stop/suspend/resume 为什么被弃用

Thread.stop() 直接抛 ThreadDeath 强制终止线程——但根本不安全:

线程持有 lock 1 → 处理一半数据 → stop() → ThreadDeath 抛出
              ↓
              ↓ lock 1 被 finally 释放
              ↓
其他线程拿到 lock 1 → 看到不一致的中间状态 → 数据混乱
1
2
3
4
5

suspend/resume 同样有死锁风险——挂起时持有的锁不释放,其他线程要这把锁就死等。

JDK 8 起正式标记 deprecated,JDK 20 起 Thread.stop() 会抛 UnsupportedOperationException——这是 Java 设计史上极少见的"删除 API"。

# 6. ThreadLocal 源码与设计

# 6.1 数据结构:ThreadLocalMap 在 Thread 上

很多人想当然以为 ThreadLocal 内部有个 Map<Thread, Object>——错。真实结构是反过来的:

Thread t1                         Thread t2
  │                                  │
  │ threadLocals (ThreadLocalMap)    │ threadLocals
  │                                  │
  ↓                                  ↓
┌────────────────────┐            ┌────────────────────┐
│ Entry[16]          │            │ Entry[16]          │
│  ├ Entry(tl1, A)   │            │  ├ Entry(tl1, X)   │
│  ├ Entry(tl2, B)   │            │  ├ Entry(tl3, Y)   │
│  └ ...             │            │  └ ...             │
└────────────────────┘            └────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
  • ThreadLocalMap 是 Thread 实例的字段——每线程一个 Map
  • Map 的 Key = ThreadLocal 对象本身(弱引用包装)
  • Map 的 Value = 具体值(强引用)

这个结构的设计目的:

  • 线程隔离天然成立(数据物理上挂在自己线程对象里)
  • ThreadLocal 对象可以被多个线程共享(作为 Key),但每个线程读写的 Value 互相独立

# 6.2 Entry 的 WeakReference 设计

static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;                                   // ★ Value 是强引用
    Entry(ThreadLocal<?> k, Object v) {
        super(k);                                    // ★ Key 是弱引用
        value = v;
    }
}
1
2
3
4
5
6
7

为什么 Key 用弱引用:当 ThreadLocal 对象在外部不再被引用时(如方法结束局部变量出栈),希望 Entry 的 Key 能被 GC 自动回收,这样就能识别"已失效的 Entry"。

但这个设计本身就埋了第 7 章的坑——继续看下去。

# 6.3 开放寻址 + 黄金分割哈希

ThreadLocalMap 不用链表解决冲突,而用开放寻址:

private static final int HASH_INCREMENT = 0x61c88647;  // ★ 黄金分割数

private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);                 // 线性探测下一格
}

private static int prevIndex(int i, int len) {
    return ((i - 1 >= 0) ? i - 1 : len - 1);            // 线性探测上一格
}
1
2
3
4
5
6
7
8
9

为什么用黄金分割数 0x61c88647:理论保证哈希值均匀分布,对长度为 2^N 的桶可达到几乎完美的散列效果。每个新创建的 ThreadLocal threadLocalHashCode = nextHashCode() 通过累加这个魔数得到。

为什么不用链表:因为单个线程的 ThreadLocal 数量通常 < 10,开放寻址在小规模下更快、内存更紧凑、不需要额外的 Node 对象。

# 6.4 set/get/remove 源码追踪

set 流程:

private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len - 1);
    
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();
        if (k == key)        { e.value = value; return; }     // 命中
        if (k == null)       { replaceStaleEntry(key, value, i); return; }  // ★ 探测到过期 Entry 顺便清理
    }
    
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

核心机制:set 时顺便清理 stale entry(key 被 GC 的 Entry)——所谓"主动清理 + 被动清理"组合,这是 Doug Lea 的精妙设计。

get 流程——同样会触发清理:

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) return (T) e.value;
    }
    return setInitialValue();          // 调 initialValue() 兜底
}
1
2
3
4
5
6
7
8
9

remove 流程——这是避免泄漏的唯一可靠手段:

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null) m.remove(this);
}

private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int i = key.threadLocalHashCode & (tab.length - 1);
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, tab.length)]) {
        if (e.get() == key) {
            e.clear();                  // 清除 Key 引用
            expungeStaleEntry(i);       // ★ 清除 Value + 重哈希
            return;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 7. ThreadLocalMap 内存泄漏真凶

# 7.1 Key 弱引用 + Value 强引用的非对称

回到 §1.1 案例的根因图:

           Thread (强引用)
                │
                ↓ threadLocals
           ThreadLocalMap
                │
                ↓
      Entry[i]
        │       │
   弱引用       强引用
        │       │
        ↓       ↓
  ThreadLocal  Value (UserInfo, 10KB)
        ↑
   外部引用消失后
   ThreadLocal 被 GC → key 变 null
   但 Value 仍然存活!  ★★★ 泄漏点 ★★★
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

关键悖论:

  • ✅ Key 设计为弱引用,意图是 ThreadLocal 被回收后 Entry 自动失效
  • ❌ Value 是强引用,Key 没了 Value 仍然挂在 Entry 上
  • ❌ 而 Entry 又被 ThreadLocalMap 强引用,ThreadLocalMap 被 Thread 强引用——只要 Thread 不死,Value 永远不死

# 7.2 线程池场景下的链式泄漏

普通线程:run 结束后线程销毁 → ThreadLocalMap 整体 GC → Value 被回收 → 没事。

线程池场景就完全不同:

Tomcat 线程池 200 个线程,每个常驻
              │
请求 1 进入   │
              ├── ThreadLocal.set(UserInfo_A)    → Entry 留下
请求 1 结束   │
              │
请求 2 进入   │
              ├── ThreadLocal.set(UserInfo_B)    → Entry 覆盖
请求 2 结束   │
              │
... 一万次请求 ...
              │
ThreadLocal 对象本身仍然是 static 字段,永远不会被回收
→ Key 永远不为 null
→ expungeStaleEntry 永远不会触发
→ 所有历史 UserInfo 全都积累在 Map 里 ★ OOM
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

但即使 ThreadLocal 是局部对象(Key 弱引用回收),Value 仍可能泄漏——因为 expungeStaleEntry 只在恰好哈希到那一格时才触发,懒清理覆盖不全。

# 7.3 五种规避方案

方案 做法 评价
方案一:用完必 remove try { tl.set(...); ... } finally { tl.remove(); } ⭐⭐⭐⭐⭐ 最稳妥
方案二:用 Filter/Interceptor Web 框架统一拦截器里 finally remove 工程上可推广
方案三:限制 Value 大小 Value 不存大对象,只存 ID 等 治标不治本
方案四:用 ScopedValue(JDK 21+) ScopedValue.where(KEY, val).run(...) 现代替代品,自动清理
方案五:避免线程池里的 ThreadLocal 改用方法参数显式传递 极端但有效

ScopedValue(JEP 446)是 JDK 21 给出的官方答案——专为虚拟线程时代设计:

// 旧的 ThreadLocal 用法(容易泄漏)
private static final ThreadLocal<UserInfo> CTX = new ThreadLocal<>();
CTX.set(user);
try { service.call(); } finally { CTX.remove(); }

// 新的 ScopedValue 用法(结构化、自动清理)
private static final ScopedValue<UserInfo> CTX = ScopedValue.newInstance();
ScopedValue.where(CTX, user).run(() -> {
    service.call();    // 内部 CTX.get() 拿到值
});                    // ★ 离开 scope 自动失效,绝对不会泄漏
1
2
3
4
5
6
7
8
9
10

# 8. InheritableThreadLocal 与 TTL

# 8.1 父子线程传递机制

// Thread 构造函数(简化)
private Thread(...) {
    Thread parent = currentThread();
    if (inheritThreadLocals && parent.inheritableThreadLocals != null) {
        this.inheritableThreadLocals =
            ThreadLocalMap.createInheritedMap(parent.inheritableThreadLocals);
    }
}
1
2
3
4
5
6
7
8

关键点:

  • 复制时机是 new Thread() 那一刻——不是运行时
  • 复制方式是浅拷贝——父子持有同一份 Value 引用
  • 普通 ThreadLocal 不复制,InheritableThreadLocal 才复制

# 8.2 线程池场景为什么失效

回到 §1.2 MDC 丢失案例:

请求 1 → main 线程 set(traceId_1)
       → 线程池里取出 worker_5
       → worker_5 在线程池创建时已经从【创建它的那个线程】继承了 ThreadLocal
       → 不是从【submit 的那个线程】继承!
       → ★ 父子继承在线程池里被"剪断"了
1
2
3
4
5

根因:线程池中的 worker 线程是线程池启动时创建的,那时 main 线程还没 set traceId——继承时机不对。

# 8.3 阿里 TransmittableThreadLocal

阿里开源的 TransmittableThreadLocal (opens new window) 通过装饰 Runnable/Callable 解决:

TransmittableThreadLocal<String> CTX = new TransmittableThreadLocal<>();
CTX.set("traceId_1");

Runnable task = () -> System.out.println(CTX.get());

// 关键:用 TtlRunnable 包装
executor.submit(TtlRunnable.get(task));   // ✅ 子线程拿得到 traceId_1

// 或者用 TtlExecutors 一次性装饰
ExecutorService ttlExecutor = TtlExecutors.getTtlExecutorService(executor);
ttlExecutor.submit(task);                  // ✅ 自动透传
1
2
3
4
5
6
7
8
9
10
11

核心原理:

  1. TtlRunnable.get(task) 时捕获当前线程的所有 TTL 值
  2. submit 后 worker 线程执行 task 前重放这些值(set 到 worker 的 ThreadLocal)
  3. 任务结束后还原worker 原本的值

配合 Java Agent 用法(卷四 33 篇钩子):

java -javaagent:transmittable-thread-local-2.x.jar -jar app.jar
1

加上 Agent 后所有标准 Executor 自动透传——业务代码完全无感,这就是 Java Agent 在生产中最经典的应用之一。

JDK 21 时代的替代品:上节提到的 ScopedValue + 结构化并发(StructuredTaskScope)天然支持跨线程传递,未来会逐步替代 TTL。

# 9. 综合回扣与卷五开篇

# 9.1 案例真相揭晓

① §1.1 ThreadLocal OOM 真相:Tomcat 线程池常驻 200 worker,每次请求 CTX.set(user) 后都不 remove()。虽然 Entry 的 Key 是弱引用,但 UserContext.CTX 是 static 强引用,Key 永远不为 null,弱引用机制完全不触发。Value (UserInfo) 持续累积——百万请求后 Map 里塞了百万 Entry × 10KB = 老年代爆炸。根治:在 Filter 末尾 finally CTX.remove(),或迁移到 JDK 21 ScopedValue。

② §1.2 MDC 丢失真相:MDC 内部用 InheritableThreadLocal,但 CompletableFuture.runAsync 默认用的是 ForkJoinPool.commonPool——线程是池启动时创建,那时 traceId 还没 set,继承时机不对。根治:用 TransmittableThreadLocal + TtlExecutors.getTtlExecutorService(commonPool) 装饰,或在 JDK 21 用 StructuredTaskScope + ScopedValue。

③ 5 大追问全部作答:

追问 答案 章节
① 6 态状态机如何流转 NEW/RUNNABLE/BLOCKED/WAITING/TIMED_WAITING/TERMINATED 全图 §2.3
② start 二次调用为何报错 threadStatus 字段守卫 + OS 层 pthread 已销毁 §3.1
③ interrupt 真实做了什么 仅设置 boolean 标志,需要线程主动响应 §5.1、5.2
④ 弱引用为何仍泄漏 Key 弱+Value 强 + ThreadLocal 是 static + 池化 worker 不死 §7.1、7.2
⑤ TTL 凭什么能跨线程 装饰 Runnable,submit 时捕获、运行前重放、结束后还原 §8.3

# 9.2 设计哲学回扣

1. "线程"是一个昂贵的物理对象:每个 Java Thread 都对应一个 OS 线程 + 1MB 栈空间 + 内核调度单元(§2.2、§3.2)。这就是为什么"线程池 + 复用"是 Java 并发 30 年的主旋律——也正因为太贵,JDK 21 才痛下决心做虚拟线程(卷五 41 篇)。这给我们的启示是:在性能优化里,永远要分清"逻辑成本"和"物理成本"——逻辑上一句 new Thread(...).start() 看似很轻,物理上却要走完整 OS 系统调用、栈分配、内核数据结构创建。

2. "标志位 + 协作中断"优于"强制终止":JDK 早期的 Thread.stop() 走"强行终止"路线,在持锁时强行抛异常导致数据腐败(§5.4);JDK 后来彻底翻盘,把它改成协作式中断——只设标志位,让线程自己择机响应。这是 30 年来 Java 设计哲学最大的转向之一:从"我能让你死"到"我请你结束"。这种"协作优于强制"的思想贯穿后续所有同步原语——AQS 用条件队列协作(36 篇)、CAS 用乐观协作(38 篇)、虚拟线程用 yieldpoint 协作(41 篇)。强制带来的是确定性陷阱,协作带来的是不变量保证。

3. "弱引用 + 主动清理"是 Java 缓存设计的经典范式:ThreadLocalMap 的 Entry = WeakReference Key + 强引用 Value + set/get 时顺便 expunge stale entry(§6.4)——这套组合是 Doug Lea 的招牌设计。同样的范式还能在 ClassValue(13 篇)、WeakHashMap、Cache 实现里看到。核心思想:让 GC 帮你做"自动失效",但不要完全依赖它做"自动清理"——主动清理是兜底,弱引用是优化。这给我们的启示是:任何"自动机制"都不是银弹,背后必须有人工兜底;ThreadLocal 也好、Spring AOP 自调用也好(卷四 34 篇)、连接池泄漏也好——所有"自动"都是表象,真相是"自动 + 兜底"。

# 9.3 卷五十篇知识地图

                           卷五:并发编程深水区(10 篇)
                                       │
       ┌───────────────────────────────┼───────────────────────────────┐
       │                               │                               │
   ★★ 地基层 ★★                  同步原语层                       现代化层
       │                               │                               │
       ↓                               ↓                               ↓
   35.Thread (本篇)               36.AQS                          40.Loom 基础
   生命周期 + ThreadLocal         同步框架地基                    虚拟线程入门
       ↑                               │                               │
       │                               ↓                               ↓
   08.synchronized                37.ReentrantLock              41.Loom 进阶
   锁升级 (已写)                   StampedLock                   Pinning + 调度
       │                               │                               │
       │                               ↓                               │
   20.HashMap 死链                38.CAS & Atomic                      │
   多线程问题 (已写)               Unsafe & VarHandle                   │
       │                               │                               │
       │                               ↓                               │
   24.CompletableFuture          39.线程池源码 ─────────────────────┘
   异步组合 (已写)                ThreadPoolExecutor
                                       │
                                       ↓
                                 (拼成完整并发体系)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

卷五写作顺序:35(地基)→ 36(AQS)→ 37(Lock 三剑客)→ 38(CAS)→ 39(线程池)→ 40(Loom 基础)→ 41(Loom 进阶 + Pinning),剩下还有 1 篇暂未规划。

# 9.4 速查表

Thread 6 态对照:

状态                 触发条件                        恢复条件
NEW                 new Thread()                    start()
RUNNABLE            start() / 调度                  ──
BLOCKED             synchronized 抢锁失败           抢到锁
WAITING             wait/join/park(无超时)        notify/被 join 的线程死/unpark
TIMED_WAITING       wait(t)/sleep(t)/join(t)/parkNanos  超时 / 被唤醒
TERMINATED          run 退出                        终态
1
2
3
4
5
6
7

ThreadLocal 三大族对比:

ThreadLocal             ── 线程隔离,无父子继承
InheritableThreadLocal  ── new Thread 时浅拷贝父线程值
TransmittableThreadLocal── 解决线程池场景,需配合 TtlRunnable / TtlExecutors
ScopedValue (JDK 21+)    ── 结构化、自动清理、虚拟线程友好
1
2
3
4

interrupt 标准模板:

while (!Thread.currentThread().isInterrupted()) {
    try {
        doWork();
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();   // 必须恢复
        break;
    }
}
1
2
3
4
5
6
7
8

ThreadLocal 防泄漏铁律:

try {
    threadLocal.set(value);
    // ... 业务逻辑
} finally {
    threadLocal.remove();              // ★ 永远不要忘
}
1
2
3
4
5
6

🎯 下一篇预告:第 36 篇《AQS 同步框架源码》——从"为什么 ReentrantLock 比 synchronized 更灵活"切入,深挖 AQS 的 state + CLH 队列双核心、acquire/release 模板方法、独占模式与共享模式、Condition 与 await/signal 实现机制、CountDownLatch/Semaphore/CyclicBarrier 如何在 AQS 之上 5 行代码实现,把 Doug Lea 的并发框架精髓彻底打透。

上次更新: 2026/06/10, 11:13:41
线程池核心源码设计
AQS同步框架源码

← 线程池核心源码设计 AQS同步框架源码→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式