Thread线程生命周期
# 34.Thread线程生命周期
# 目录介绍
- 1. 案例引入
- 2. Thread 整体架构
- 3. start 与 run 源码
- 4. join/sleep/yield 与协作
- 5. interrupt 三件套
- 6. ThreadLocal 源码与设计
- 7. ThreadLocalMap 内存泄漏真凶
- 8. InheritableThreadLocal 与 TTL
- 9. 综合回扣与卷五开篇
# 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);
}
}
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();
}
}
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 问题)
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
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; // 协程载体
}
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 核心 │
└──────────────────────┘
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 // 终止
}
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 │
└────────────┘
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();
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() 业务代码执行
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,没启新线程
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);
}
}
}
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 时重新竞争
}
2
3
4
5
6
7
# 4.3 yield 的"建议性"语义
public static native void yield();
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();
}
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()
2
3
4
5
6
7
8
响应中断的标准模板:
while (!Thread.currentThread().isInterrupted()) {
try {
// 执行任务
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // ★ 关键:恢复中断状态
break; // 优雅退出
}
}
2
3
4
5
6
7
8
9
为什么 catch 后还要 interrupt():因为抛 InterruptedException 时 JVM 已自动清除标志位——如果上层代码依赖该标志判断终止,必须手动恢复。
# 5.3 isInterrupted vs interrupted 的陷阱
public boolean isInterrupted() // 实例方法,不清除标志
public static boolean interrupted() // 静态方法,★ 检查并清除标志
2
踩坑示例:
Thread.currentThread().interrupt();
System.out.println(Thread.interrupted()); // true
System.out.println(Thread.interrupted()); // false ← ★ 已被清除
System.out.println(Thread.currentThread().isInterrupted()); // false
2
3
4
记忆口诀:带 is 的不清除,不带 is 的清除。
# 5.4 stop/suspend/resume 为什么被弃用
Thread.stop() 直接抛 ThreadDeath 强制终止线程——但根本不安全:
线程持有 lock 1 → 处理一半数据 → stop() → ThreadDeath 抛出
↓
↓ lock 1 被 finally 释放
↓
其他线程拿到 lock 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) │
│ └ ... │ │ └ ... │
└────────────────────┘ └────────────────────┘
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;
}
}
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); // 线性探测上一格
}
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();
}
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() 兜底
}
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;
}
}
}
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 仍然存活! ★★★ 泄漏点 ★★★
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
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 自动失效,绝对不会泄漏
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);
}
}
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 的那个线程】继承!
→ ★ 父子继承在线程池里被"剪断"了
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); // ✅ 自动透传
2
3
4
5
6
7
8
9
10
11
核心原理:
TtlRunnable.get(task)时捕获当前线程的所有 TTL 值- submit 后 worker 线程执行
task前重放这些值(set 到 worker 的 ThreadLocal) - 任务结束后还原worker 原本的值
配合 Java Agent 用法(卷四 33 篇钩子):
java -javaagent:transmittable-thread-local-2.x.jar -jar app.jar
加上 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
│
↓
(拼成完整并发体系)
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 退出 终态
2
3
4
5
6
7
ThreadLocal 三大族对比:
ThreadLocal ── 线程隔离,无父子继承
InheritableThreadLocal ── new Thread 时浅拷贝父线程值
TransmittableThreadLocal── 解决线程池场景,需配合 TtlRunnable / TtlExecutors
ScopedValue (JDK 21+) ── 结构化、自动清理、虚拟线程友好
2
3
4
interrupt 标准模板:
while (!Thread.currentThread().isInterrupted()) {
try {
doWork();
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 必须恢复
break;
}
}
2
3
4
5
6
7
8
ThreadLocal 防泄漏铁律:
try {
threadLocal.set(value);
// ... 业务逻辑
} finally {
threadLocal.remove(); // ★ 永远不要忘
}
2
3
4
5
6
🎯 下一篇预告:第 36 篇《AQS 同步框架源码》——从"为什么 ReentrantLock 比 synchronized 更灵活"切入,深挖 AQS 的 state + CLH 队列双核心、acquire/release 模板方法、独占模式与共享模式、Condition 与 await/signal 实现机制、CountDownLatch/Semaphore/CyclicBarrier 如何在 AQS 之上 5 行代码实现,把 Doug Lea 的并发框架精髓彻底打透。