Skip to content

线程模型与生命周期 - 并发 | 炼金塔


1.1 为什么要深入理解线程模型?

Section titled “1.1 为什么要深入理解线程模型?”
  • 面试核心考点:线程状态、阻塞与等待是中高频面试题
  • 性能优化基础:理解线程创建成本,才能正确使用线程池
  • 并发问题排查:掌握线程状态才能分析死锁、活锁等问题
场景痛点解决方案
高并发请求处理创建线程成本高线程池复用
用户Session管理线程间数据隔离ThreadLocal
异步任务处理阻塞导致吞吐下降虚拟线程/异步
定时任务调度线程资源浪费调度线程池

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() 执行完毕 / 异常
TERMINATED
对比项BLOCKEDWAITING / 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 内置锁时才会出现。


// 方式一:继承 Thread
class 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(); // 阻塞等待结果

Java 的线程中断是协作式的,不是强制终止。

// 请求中断(设置中断标志位)
thread.interrupt();
// 检查中断标志(不清除标志)
thread.isInterrupted();
// 检查并清除中断标志(静态方法)
Thread.interrupted();
可中断的阻塞方法(会抛出 InterruptedException):
Thread.sleep() / Object.wait() / Thread.join()
BlockingQueue.take() / Future.get()
LockSupport.park()(不抛异常,但会返回)
ReentrantLock.lockInterruptibly()
不可中断的操作:
synchronized 等锁(忽略中断,继续等待)
普通 I/O 操作(SocketChannel 例外,可被中断)
// 模式一:传播中断(向上层抛出,让调用方处理)
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 为每个线程提供独立的变量副本,常用于保存用户上下文、数据库连接等。

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。

对比项平台线程(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");
});
// 使用虚拟线程的 ExecutorService
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> processRequest());
}

Q1: 创建线程有几种方式?

🎯 考察重点: 对线程创建机制的理解深度

📝 回答要点:

从技术上讲,只有一种方式创建线程:new Thread().start()

实现 RunnableCallable、线程池本质上都是在提供任务,而不是直接创建线程:

  1. 继承 Thread:重写 run() 方法
  2. 实现 Runnable:实现 run() 方法(推荐,解耦任务与线程)
  3. 实现 Callable + Future:可获取返回值和异常

🔍 追问扩展:

  • Q: 为什么推荐使用 Runnable 而不是继承 Thread? A: 解耦任务与执行机制;Java 单继承限制;资源复用。

Q2: 线程的 BLOCKED 和 WAITING 状态有什么区别?

🎯 考察重点: 线程状态机理解、锁机制

📝 回答要点:

对比项BLOCKEDWAITING / TIMED_WAITING
触发条件争抢 synchronized 锁失败主动调用 wait/join/park
等待对象监视器锁(Monitor)由调用方决定
恢复条件持有锁的线程释放锁notify/notifyAll/unpark/超时
响应中断不响应响应(抛出 InterruptedException)

🔍 追问扩展:

  • Q: ReentrantLock.lock() 会让线程处于什么状态? A: WAITING 状态。ReentrantLock 底层使用 LockSupport.park(),使线程进入 WAITING 状态,而非 BLOCKED。

Q3: 如何优雅地停止一个线程?

🎯 考察重点: 中断机制理解、安全编程

📝 回答要点:

  1. 使用中断机制:调用 thread.interrupt() 设置中断标志
  2. 检查中断状态:在循环中检查 isInterrupted()
  3. 响应中断:在可中断方法中捕获 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()

知识点关键点面试权重
线程状态机6 种状态转换★★★★☆
OS 线程映射1:1 映射,成本高★★★★☆
线程中断协作式中断★★★★★
ThreadLocal弱引用 + 内存泄漏★★★★★
虚拟线程M:N 映射,I/O 密集★★★☆☆
  1. 掌握状态机:6 种状态及转换条件要能默写
  2. 理解锁机制:BLOCKED vs WAITING 是高频追问点
  3. 正确处理中断:两种模式要能写出代码
  4. 关注新特性:虚拟线程是 Java 21+ 必备知识
  • 《Java 并发编程实战》第 7 章:取消与关闭
  • JDK 源码:Thread.javaThreadLocal.java
  • JEP 444: Virtual Threads(Java 21)