Java 线程从生到死:创建三式、六态流转、中断协作与死锁活锁饥饿,一篇讲透

文章收录专栏:Java 核心原理全解:源码・并发・面试实战

系列文章:

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 只是递纸条,响应不响应由线程自己决定;死锁是资源环,活锁是谦让死,饥饿是插队死 ------ 三者的破解思路分别是 "有序申请、退避不对称、公平排队"。

相关推荐
Wang's Blog3 小时前
Java框架 SpringCloud 快速入门: Nacos 服务多级存储模型
java·spring·spring cloud
小羊没烦恼!3 小时前
系统内部模块(子系统)之间的耦合以及模块(子系统)划分
java·开发语言·前端·数据库·算法·c#
Chase_______3 小时前
【杂项知识点】一文搞懂 JVM、JRE 与 JDK:从概念混淆到生产部署
java·开发语言·jvm
Nebula_g3 小时前
JavaSE拓展:Arrays和Collections工具类
java·开发语言·算法·排序算法·工具类·javase·技术栈
一个有温度的技术博主3 小时前
凌晨三点的消息堆积:当 MQ 变成“停车场“
java·后端·场景
成旭先生4 小时前
AI 文本审核 API:一段中文文本判风险等级、命中标签与处置建议
java·前端·人工智能·api接口·内容风控·文本审核·ugc审核
~~~4554 小时前
Java 数组详细笔记
java
Jinkxs4 小时前
Zookeeper - Java API 实现节点的创建与删除开发
java·zookeeper·java-zookeeper
liangshanbo12154 小时前
前端面试题:AI 对话中超长消息导致内存溢出,怎么解决?
java·开发语言·前端