3、线程生命周期管理:线程的启动、运行、阻塞与死亡状态、daemon守护线程详解、线程的join方法深度解析
聊到线程,很多人第一反应就是「new一个Thread,然后start()」。嗯,没错,但线程从创建到销毁,中间经历了什么?我见过不少新手,线程一多就乱套,根本不知道哪个线程在干嘛。说白了,你得先搞懂线程的「一生」。
3.1 线程的五大状态
Java线程的生命周期,官方文档里定义了6种状态(Thread.State枚举),但咱们从实战角度,可以归纳为5个核心阶段:新建、就绪、运行、阻塞、死亡。
我个人习惯把这五个状态画成一个闭环图,方便理解:
这张图我建议你存下来。每次写多线程代码时,想想当前线程在哪个位置,很多bug就能提前发现。
3.2 线程的启动与运行
启动线程,唯一正确的方式就是调用start()。为什么不能直接调run()?
你想想看,直接调run(),它就是在当前线程里执行一个普通方法,根本没创建新线程。我见过有人把run()当启动方法用,结果程序跑得跟单线程一样慢,还纳闷为什么没加速。
// 正确启动
Thread t = new Thread(() -> System.out.println("新线程执行"));
t.start(); // 这才是真正的启动
// 错误示范
t.run(); // 这只是在主线程里调用方法,不是多线程!
核心要点:start()会告诉JVM创建一个新线程,并让新线程去执行run()里的代码。run()只是普通方法调用,不会创建新线程。
3.3 阻塞状态详解
线程进入阻塞,说白了就是「暂时歇着,等条件满足了再干」。阻塞的原因主要有三种:
- 等待阻塞:调用了
wait(),线程会释放锁,进入等待队列 - 同步阻塞:竞争锁失败,进入同步队列
- 其他阻塞:调用了
sleep()、join(),或者发生了IO操作
我在项目中遇到过一个问题:一个线程调了sleep(5000),但业务要求中途取消。后来才知道,sleep()期间调用interrupt()会抛出InterruptedException,用这个机制就能优雅地唤醒线程。
小技巧:sleep()不会释放锁,wait()会释放锁。面试常考,实战也常用。
3.4 线程的死亡
线程死亡,就是run()方法执行完毕,或者抛出了未捕获的异常。注意,线程一旦死亡,就不能再调用start()了,否则会抛IllegalThreadStateException。
嗯,这里有个坑:stop()方法已经被标记为废弃了。为什么?因为它会强制杀死线程,导致资源没释放、数据不一致。我曾经在一个支付系统里看到有人用stop(),结果账户余额对不上,排查了一整天才发现是线程被暴力终止了。
警告:永远不要使用Thread.stop()、Thread.suspend()、Thread.resume()。它们不安全,已经被标记为废弃方法。
3.5 Daemon守护线程详解
守护线程,说白了就是「后台小弟」。JVM会在所有非守护线程都结束后,自动退出——不管守护线程有没有跑完。
举个例子:垃圾回收线程就是守护线程。你不可能让JVM因为GC线程还在跑就不退出吧?
Thread daemonThread = new Thread(() -> {
while (true) {
System.out.println("守护线程在后台工作...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
break;
}
}
});
daemonThread.setDaemon(true); // 设置为守护线程
daemonThread.start();
System.out.println("主线程结束");
// 主线程结束后,JVM会立即退出,守护线程也会被强制终止
关键点:setDaemon(true)必须在start()之前调用。否则会抛IllegalThreadStateException。
我个人习惯把守护线程用在后台监控、日志清理、心跳检测这些场景。但要注意,守护线程里别做重要的数据写入操作——因为JVM退出时不会等它。
3.6 Join方法深度解析
join()的作用,就是让当前线程「等待」目标线程执行完毕。说白了,就是线程间的同步工具。
你想想看,主线程启动了子线程去下载文件,但主线程需要等文件下载完才能处理。这时候就该用join()。
Thread downloadThread = new Thread(() -> {
System.out.println("开始下载...");
try {
Thread.sleep(3000); // 模拟下载
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("下载完成");
});
downloadThread.start();
System.out.println("主线程等待下载完成...");
downloadThread.join(); // 主线程阻塞,直到downloadThread结束
System.out.println("主线程继续执行");
join()还有两个重载版本:
| 方法 | 说明 |
|---|---|
| join() | 无限等待,直到目标线程结束 |
| join(long millis) | 最多等待millis毫秒,超时后不再等待 |
| join(long millis, int nanos) | 更精确的超时控制,一般用不上 |
我曾经在写一个批量处理系统时,用join(5000)给每个子线程设了5秒超时。结果发现有个线程卡住了,但系统没有死等,而是超时后继续处理下一个任务。这就是join带超时的好处——避免永久阻塞。
实战建议:生产环境中尽量使用带超时的join(),防止线程死锁导致整个应用挂起。
3.7 综合示例:线程生命周期管理
最后,咱们来个综合例子,把今天讲的知识点串起来:
public class ThreadLifecycleDemo {
public static void main(String[] args) throws InterruptedException {
// 创建线程(新建状态)
Thread worker = new Thread(() -> {
try {
System.out.println("子线程:开始工作");
Thread.sleep(2000); // 阻塞状态
System.out.println("子线程:工作完成");
} catch (InterruptedException e) {
System.out.println("子线程:被中断");
}
});
// 设置为守护线程
worker.setDaemon(false);
// 启动线程(就绪 -> 运行)
worker.start();
System.out.println("主线程:等待子线程...");
// 主线程等待子线程(主线程进入阻塞)
worker.join(3000);
System.out.println("主线程:检查子线程状态");
System.out.println("子线程是否存活: " + worker.isAlive());
System.out.println("子线程状态: " + worker.getState());
// 子线程执行完毕后进入死亡状态
System.out.println("主线程:结束");
}
}
运行这段代码,你会看到线程从新建到死亡的全过程。建议你动手跑一跑,把setDaemon和join的参数改一改,看看效果有什么不同。
好了,线程生命周期管理就讲到这里。记住这五个状态、守护线程的特性、以及join的正确用法,你的多线程编程就能少踩很多坑。
一句话总结:线程从新建到死亡,中间经历就绪、运行、阻塞的循环;守护线程是后台小弟,JVM退出时不会等它;join是线程间的同步利器,记得加超时。
可可精品资料库