
文章收录专栏:Java 核心原理全解:源码・并发・面试实战
系列文章:
深入理解 Java 内存模型(JMM):从抽象规范到生产实践
衔接引子 :前文我们搞懂了锁(synchronized/AQS)和 JMM 内存模型,但还有一个最基础的问题没回答 ------线程本身是怎么来的、怎么活的、怎么死的。这一篇从 "生" 讲到 "死":创建、状态、中断、守护,最后落到并发里最经典的三个问题:死锁、活锁、饥饿。
1. 线程创建三式
| 方式 | 核心 | 特点 |
|---|---|---|
| 继承 Thread | 重写 run(),start() 启动 |
受单继承限制,不推荐 |
| 实现 Runnable | 实现 run() |
无返回值、不能抛受检异常 |
| 实现 Callable | 实现 call() |
有返回值、可抛受检异常,配 Future |
表格里没有列线程池,是有意为之:创建线程有三式,管理线程靠线程池。线程池复用的还是 Runnable/Callable 的任务模型,把它当成 "第四种创建方式" 不严谨 ------ 它的本质是线程复用与调度。具体怎么配参数、怎么防 ThreadLocal 泄漏,下一篇《线程池与 ThreadLocal》见。
// ① 继承 Thread(不推荐)
Thread t1 = new Thread() {
@Override public void run() { System.out.println("继承方式"); }
};
t1.start();
// ② 实现 Runnable(通用)
Runnable task = () -> System.out.println("Runnable 方式");
new Thread(task).start();
// ③ Callable + FutureTask
Callable<Integer> call = () -> 42;
FutureTask<Integer> ft = new FutureTask<>(call);
new Thread(ft).start();
System.out.println(ft.get()); // 阻塞拿结果
// ④ 生产首选:线程池 + submit
ExecutorService pool = Executors.newFixedThreadPool(2);
Future<Integer> f = pool.submit(() -> 42);
pool.shutdown();
高频考点 :execute() 与 submit() 的区别 ------execute 只收 Runnable,异常直接抛或交给 UncaughtExceptionHandler;submit 收 Runnable/Callable,返回 Future,异常封装在 Future.get() 里 ,调用 get 时才抛 ExecutionException。
2. 六态流转
NEW → RUNNABLE(就绪+运行)→ BLOCKED / WAITING / TIMED_WAITING → TERMINATED
| 方法 | 是否需持锁 | 是否释放锁 | 状态 | 唤醒 |
|---|---|---|---|---|
| Thread.sleep | 否 | 不释放已持有的锁 | TIMED_WAITING | 时间到 |
| Object.wait | 是 | 释放锁 | WAITING | notify/notifyAll |
| LockSupport.park | 否(不涉及锁) | 不操作锁 | WAITING | unpark |
易混点 :LockSupport.park 本身不获取锁,谈不上 "释放锁";它中断时不抛异常,只置中断标记。
3. 中断是协作机制,不是强杀
Thread worker = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
try { Thread.sleep(100); }
catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 标记被清除,需重新置位
break;
}
}
});
worker.start();
worker.interrupt(); // 只置标记,线程自行响应退出
三个方法 :interrupt() 置标记;isInterrupted() 查询不清除;interrupted() 查询并清除(静态方法)。阻塞方法收到中断抛 InterruptedException 并清除标记 ,所以捕获后要重新 interrupt() 再退出。
4. 守护线程与未捕获异常
-
setDaemon(true):服务于用户线程,JVM 退出不等待它。 -
UncaughtExceptionHandler:防止线程静默死亡,统一记日志:Thread t = new Thread(() -> { throw new RuntimeException("boom"); });
t.setUncaughtExceptionHandler((th, ex) -> log.error("线程 {} 崩溃", th.getName(), ex));
5. 死锁、活锁与线程饥饿
死锁四大必要条件(缺一不可):互斥、请求并持有、不可剥夺、循环等待。
Object a = new Object(), b = new Object();
Thread t1 = new Thread(() -> {
synchronized (a) { sleep(50); synchronized (b) { } }
});
Thread t2 = new Thread(() -> {
synchronized (b) { sleep(50); synchronized (a) { } }
});
破解策略 :破坏请求并持有(一次性申请全部资源);破坏不可剥夺(tryLock 超时主动释放);破坏循环等待(资源编号按序申请)。
排查 :jstack -l <pid>、jcmd <pid> Thread.print(JDK8+ 推荐)、Arthas thread -b(定位阻塞线程,不保证一定是死锁环)、JFR 持续监控。
活锁:线程都在运行但反复互让重试(tryLock 失败 yield 的对称谦让)。解决:随机 / 指数退避、限制重试次数、线程 ID 排序等不对称策略。
饥饿 :非公平锁持续插队导致长期拿不到资源。ReentrantLock 公平锁可很大程度避免;synchronized 只有非公平实现,无法解决。
6. 虚拟线程(JDK21,浅谈)
// JDK21:轻量级线程,每次新建,勿池化
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> handleIo());
}
Thread.ofVirtual().start(() -> handleIo());
- Pinning:长时间持有 synchronized 或执行本地方法时无法卸载,会阻塞载体线程;长临界区推荐 ReentrantLock。
- 不适合 CPU 密集任务;大量阻塞 IO 场景收益最大。
金句速记
线程是 "协作" 不是 "抢占":interrupt 只是递纸条,响应不响应由线程自己决定;死锁是资源环,活锁是谦让死,饥饿是插队死 ------ 三者的破解思路分别是 "有序申请、退避不对称、公平排队"。
