线程模型与生命周期 - 并发 | 炼金塔
线程模型与生命周期
Section titled “线程模型与生命周期”一、引入背景
Section titled “一、引入背景”1.1 为什么要深入理解线程模型?
Section titled “1.1 为什么要深入理解线程模型?”- 面试核心考点:线程状态、阻塞与等待是中高频面试题
- 性能优化基础:理解线程创建成本,才能正确使用线程池
- 并发问题排查:掌握线程状态才能分析死锁、活锁等问题
1.2 实际业务场景痛点
Section titled “1.2 实际业务场景痛点”| 场景 | 痛点 | 解决方案 |
|---|---|---|
| 高并发请求处理 | 创建线程成本高 | 线程池复用 |
| 用户Session管理 | 线程间数据隔离 | ThreadLocal |
| 异步任务处理 | 阻塞导致吞吐下降 | 虚拟线程/异步 |
| 定时任务调度 | 线程资源浪费 | 调度线程池 |
二、Java 线程与 OS 线程的映射
Section titled “二、Java 线程与 OS 线程的映射”Java Thread ↓ JNI 调用OS Kernel Thread(Linux 上即 POSIX Thread / pthread) ↓CPU 内核调度执行这一设计的代价:
- 创建/销毁成本高(需要系统调用,内核分配 TCB、栈空间,默认栈大小 512KB~1MB)
- 线程数上限受 OS 限制(Linux 默认约 32768 个)
- 大量线程时上下文切换开销显著
这也是线程池存在的核心原因。
Java 线程有 6 种状态,定义在 Thread.State 枚举中:
NEW │ start() ▼RUNNABLE ◄──────────────────────────────────────┐ │ │ │ 等待 synchronized 锁 │ 锁可用 ▼ │BLOCKED ─────────────────────────────────────────┘
│ Object.wait() / Thread.join() / LockSupport.park() ▼WAITING ──────────────────────────────────────────┐ │ notify/notifyAll │ wait(timeout) / join(timeout) / parkNanos() │ unpark() ▼ │TIMED_WAITING ────────────────────────────────────┘
│ run() 执行完毕 / 异常 ▼TERMINATEDBLOCKED vs WAITING 的关键区别
Section titled “BLOCKED vs WAITING 的关键区别”| 对比项 | BLOCKED | WAITING / TIMED_WAITING |
|---|---|---|
| 触发条件 | 争抢 synchronized 锁失败 | 主动调用 wait/join/park |
| 等待对象 | 监视器锁(Monitor) | 由调用方决定 |
| 恢复条件 | 持有锁的线程释放锁 | notify/notifyAll/unpark/超时 |
| CPU 消耗 | 不占用 CPU | 不占用 CPU |
| 响应中断 | 不响应(等锁过程中忽略中断) | WAITING 响应;BLOCKED 不响应 |
面试追问:线程调用
Lock.lock()时是 BLOCKED 还是 WAITING?答:是 WAITING(或 TIMED_WAITING)。
ReentrantLock底层使用LockSupport.park(),使线程进入 WAITING 状态,而非 BLOCKED。BLOCKED 只有在等待synchronized内置锁时才会出现。
线程创建的三种方式
Section titled “线程创建的三种方式”// 方式一:继承 Threadclass MyThread extends Thread { @Override public void run() { /* 任务逻辑 */ }}new MyThread().start();
// 方式二:实现 Runnable(推荐,解耦任务与线程)Thread t = new Thread(() -> { /* 任务逻辑 */ });t.start();
// 方式三:实现 Callable + Future(可获取返回值和异常)FutureTask<Integer> task = new FutureTask<>(() -> { return compute();});new Thread(task).start();Integer result = task.get(); // 阻塞等待结果线程中断机制
Section titled “线程中断机制”Java 的线程中断是协作式的,不是强制终止。
// 请求中断(设置中断标志位)thread.interrupt();
// 检查中断标志(不清除标志)thread.isInterrupted();
// 检查并清除中断标志(静态方法)Thread.interrupted();中断的响应规则
Section titled “中断的响应规则”可中断的阻塞方法(会抛出 InterruptedException): Thread.sleep() / Object.wait() / Thread.join() BlockingQueue.take() / Future.get() LockSupport.park()(不抛异常,但会返回) ReentrantLock.lockInterruptibly()
不可中断的操作: synchronized 等锁(忽略中断,继续等待) 普通 I/O 操作(SocketChannel 例外,可被中断)正确处理中断的两种模式
Section titled “正确处理中断的两种模式”// 模式一:传播中断(向上层抛出,让调用方处理)void doWork() throws InterruptedException { while (!Thread.currentThread().isInterrupted()) { Thread.sleep(1000); // 抛出 InterruptedException,标志位被清除 }}
// 模式二:恢复中断(吞掉异常后必须重新设置标志位)void doWork() { while (!Thread.currentThread().isInterrupted()) { try { Thread.sleep(1000); } catch (InterruptedException e) { // 捕获后中断标志已被清除,必须重新设置! Thread.currentThread().interrupt(); } }}反模式:
catch (InterruptedException e) { // 什么也不做 }—— 这会吞掉中断信号,导致上层代码无法感知中断,是非常常见的并发 Bug。
ThreadLocal 原理
Section titled “ThreadLocal 原理”ThreadLocal 为每个线程提供独立的变量副本,常用于保存用户上下文、数据库连接等。
Thread 对象└── threadLocals: ThreadLocal.ThreadLocalMap ├── Entry(WeakReference<ThreadLocal>, value) └── Entry(WeakReference<ThreadLocal>, value)内存泄漏的根本原因:
强引用链:Thread → ThreadLocalMap → Entry → value(强引用)弱引用:Entry.key → ThreadLocal(弱引用,GC 可回收)
当 ThreadLocal 被 GC 回收后: key = null,但 value 仍被 Entry 强引用 如果线程是线程池线程(生命周期长),value 永远不会被 GC → 内存泄漏正确使用:使用完毕后必须调用 remove(),特别是在线程池环境中。
private static final ThreadLocal<UserContext> CONTEXT = new ThreadLocal<>();
void handleRequest(UserContext ctx) { CONTEXT.set(ctx); try { doProcess(); } finally { CONTEXT.remove(); // 必须在 finally 中清理 }}虚拟线程(Virtual Threads,Java 21)
Section titled “虚拟线程(Virtual Threads,Java 21)”虚拟线程是 Project Loom 的核心成果,在 Java 21 正式 GA。
与平台线程的对比
Section titled “与平台线程的对比”| 对比项 | 平台线程(OS Thread) | 虚拟线程(Virtual Thread) |
|---|---|---|
| 映射关系 | 1:1 映射 OS 线程 | M:N 映射到少量 OS 载体线程 |
| 创建成本 | 高(~1MB 栈,系统调用) | 极低(~数KB 初始栈,堆分配) |
| 并发数量 | 数千个 | 数百万个 |
| 适用场景 | CPU 密集型 | I/O 密集型(等待为主) |
| 调度方 | OS 内核 | JVM |
虚拟线程执行 I/O 阻塞操作时: 1. JVM 检测到阻塞点(如 socket read) 2. 将虚拟线程从载体线程(ForkJoinPool worker)上 unmount 3. 载体线程继续执行其他虚拟线程 4. I/O 完成后,虚拟线程重新 mount 到某个载体线程继续执行
效果:1 个 OS 线程可以承载大量并发 I/O 等待,吞吐量接近异步编程,代码保持同步风格// 创建虚拟线程(Java 21)Thread vt = Thread.ofVirtual().start(() -> { // 同步阻塞代码,但不会浪费 OS 线程 String result = httpClient.get("https://api.example.com/data");});
// 使用虚拟线程的 ExecutorServicetry (var executor = Executors.newVirtualThreadPerTaskExecutor()) { executor.submit(() -> processRequest());}三、面试突击篇
Section titled “三、面试突击篇”3.1 基础概念题
Section titled “3.1 基础概念题”Q1: 创建线程有几种方式?
🎯 考察重点: 对线程创建机制的理解深度
📝 回答要点:
从技术上讲,只有一种方式创建线程:new Thread().start()。
实现 Runnable、Callable、线程池本质上都是在提供任务,而不是直接创建线程:
- 继承 Thread:重写
run()方法 - 实现 Runnable:实现
run()方法(推荐,解耦任务与线程) - 实现 Callable + Future:可获取返回值和异常
🔍 追问扩展:
- Q: 为什么推荐使用 Runnable 而不是继承 Thread? A: 解耦任务与执行机制;Java 单继承限制;资源复用。
3.2 原理分析题
Section titled “3.2 原理分析题”Q2: 线程的 BLOCKED 和 WAITING 状态有什么区别?
🎯 考察重点: 线程状态机理解、锁机制
📝 回答要点:
| 对比项 | BLOCKED | WAITING / TIMED_WAITING |
|---|---|---|
| 触发条件 | 争抢 synchronized 锁失败 | 主动调用 wait/join/park |
| 等待对象 | 监视器锁(Monitor) | 由调用方决定 |
| 恢复条件 | 持有锁的线程释放锁 | notify/notifyAll/unpark/超时 |
| 响应中断 | 不响应 | 响应(抛出 InterruptedException) |
🔍 追问扩展:
- Q:
ReentrantLock.lock()会让线程处于什么状态? A: WAITING 状态。ReentrantLock底层使用LockSupport.park(),使线程进入 WAITING 状态,而非 BLOCKED。
3.3 实战应用题
Section titled “3.3 实战应用题”Q3: 如何优雅地停止一个线程?
🎯 考察重点: 中断机制理解、安全编程
📝 回答要点:
- 使用中断机制:调用
thread.interrupt()设置中断标志 - 检查中断状态:在循环中检查
isInterrupted() - 响应中断:在可中断方法中捕获
InterruptedException并处理
public class Worker implements Runnable { @Override public void run() { while (!Thread.currentThread().isInterrupted()) { try { doWork(); } catch (InterruptedException e) { // 清理资源 Thread.currentThread().interrupt(); // 恢复中断状态 break; } } }}禁止使用:Thread.stop()(已废弃)—— 会强制终止线程,导致对象状态不一致。
🔍 追问扩展:
- Q: ThreadLocal 为什么会内存泄漏,如何避免?
A: Entry 对 key 是弱引用,对 value 是强引用。ThreadLocal 被 GC 后 key 为 null,但 value 仍被 Entry 引用。解决方案:使用完毕后调用
remove()。
4.1 核心要点回顾
Section titled “4.1 核心要点回顾”| 知识点 | 关键点 | 面试权重 |
|---|---|---|
| 线程状态机 | 6 种状态转换 | ★★★★☆ |
| OS 线程映射 | 1:1 映射,成本高 | ★★★★☆ |
| 线程中断 | 协作式中断 | ★★★★★ |
| ThreadLocal | 弱引用 + 内存泄漏 | ★★★★★ |
| 虚拟线程 | M:N 映射,I/O 密集 | ★★★☆☆ |
4.2 学习建议
Section titled “4.2 学习建议”- 掌握状态机:6 种状态及转换条件要能默写
- 理解锁机制:BLOCKED vs WAITING 是高频追问点
- 正确处理中断:两种模式要能写出代码
- 关注新特性:虚拟线程是 Java 21+ 必备知识
4.3 扩展阅读
Section titled “4.3 扩展阅读”- 《Java 并发编程实战》第 7 章:取消与关闭
- JDK 源码:
Thread.java、ThreadLocal.java - JEP 444: Virtual Threads(Java 21)