3、线程生命周期管理:线程的启动、运行、阻塞与死亡状态、daemon守护线程详解、线程的join方法深度解析

聊到线程,很多人第一反应就是「new一个Thread,然后start()」。嗯,没错,但线程从创建到销毁,中间经历了什么?我见过不少新手,线程一多就乱套,根本不知道哪个线程在干嘛。说白了,你得先搞懂线程的「一生」。

3.1 线程的五大状态

Java线程的生命周期,官方文档里定义了6种状态(Thread.State枚举),但咱们从实战角度,可以归纳为5个核心阶段:新建、就绪、运行、阻塞、死亡。

我个人习惯把这五个状态画成一个闭环图,方便理解:

新建 new Thread() 就绪 Runnable 运行 Running 阻塞 Blocked/Waiting 死亡 Terminated start() 获取CPU yield()/时间片用完 sleep()/wait()/IO 唤醒/IO完成 run()结束/异常

这张图我建议你存下来。每次写多线程代码时,想想当前线程在哪个位置,很多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是线程间的同步利器,记得加超时。

可可精品资料库