1. 线程生命周期概述
在Java多线程编程中,理解线程的生命周期是每个开发者必须掌握的基础知识。就像人的一生会经历不同阶段一样,Java线程从创建到销毁也会经历若干个明确的状态转换。这些状态定义在java.lang.Thread类的State枚举中,清晰地勾勒出一个线程从出生到消亡的完整轨迹。
我刚开始接触多线程时,常常困惑为什么有些线程明明调用了start()方法却迟迟不执行,或者为什么有些线程看起来已经"结束"却还能继续工作。直到系统性地梳理了线程状态机,这些问题才迎刃而解。掌握这些状态转换规律,不仅能帮助我们编写更健壮的多线程程序,还能在调试时快速定位线程阻塞或死锁的问题所在。
2. Java线程的六种状态详解
2.1 NEW(新建状态)
当使用new Thread()创建一个线程对象时,线程就处于NEW状态。这个阶段的线程就像一张白纸,虽然已经分配了内存空间,但还没有任何执行轨迹。此时线程的PCB(进程控制块)尚未创建,操作系统层面也感知不到这个线程的存在。
java复制Thread thread = new Thread(() -> {
System.out.println("线程执行体");
});
// 此时thread.getState()返回Thread.State.NEW
需要注意的是,直接调用run()方法并不会改变线程状态:
java复制thread.run(); // 仍然是NEW状态,因为是在当前线程同步执行
2.2 RUNNABLE(可运行状态)
调用start()方法后,线程进入RUNNABLE状态。这个状态其实包含两个子状态:
- 就绪(Ready):线程等待CPU时间片
- 运行中(Running):线程正在CPU上执行
java复制thread.start(); // 状态变为RUNNABLE
在Linux系统下,此时内核会调用clone()创建轻量级进程,Java线程与内核线程是1:1映射关系。线程调度由操作系统负责,开发者无法精确控制执行顺序。
注意:RUNNABLE状态下的线程可能因为时间片用完而回到就绪队列,这对开发者是透明的。
2.3 BLOCKED(阻塞状态)
当线程试图获取一个被其他线程持有的对象锁时,会进入BLOCKED状态。这种情况通常发生在同步代码块或方法中:
java复制synchronized(lockObj) {
// 如果其他线程持有lockObj,当前线程会阻塞
}
BLOCKED状态与下面要讲的WAITING状态不同,它是被动发生的,且只能通过获取锁来解除。在实际项目中,过多的BLOCKED线程往往是性能瓶颈的信号。
2.4 WAITING(无限期等待)
线程主动调用以下方法会进入WAITING状态:
- Object.wait()
- Thread.join()
- LockSupport.park()
java复制synchronized(lockObj) {
lockObj.wait(); // 释放锁并进入WAITING状态
}
WAITING状态的线程需要其他线程通过notify()/notifyAll()或中断来唤醒。我在实际项目中曾遇到过一个典型问题:某个线程调用了wait()但没有任何地方调用notify(),导致线程永远挂起,这就是典型的线程泄漏。
2.5 TIMED_WAITING(限期等待)
与WAITING类似,但带有超时参数:
- Thread.sleep(long)
- Object.wait(long)
- Thread.join(long)
- LockSupport.parkNanos()
- LockSupport.parkUntil()
java复制Thread.sleep(1000); // TIMED_WAITING状态1秒
这个状态在定时任务、轮询检查等场景很常见。需要注意的是,sleep()不会释放锁,而wait()会释放锁。
2.6 TERMINATED(终止状态)
线程执行完run()方法或抛出未捕获异常时进入TERMINATED状态。此时线程对象仍然存在,可以调用其方法,但不能再通过start()重启。
java复制thread.start();
// 等待线程执行完毕
thread.join();
System.out.println(thread.getState()); // TERMINATED
3. 线程状态转换全图解
让我们通过一个完整的示例演示所有状态转换:
java复制public class ThreadLifecycleDemo {
private static final Object lock = new Object();
public static void main(String[] args) throws Exception {
Thread thread = new Thread(() -> {
synchronized (lock) {
try {
System.out.println("线程获取锁,进入RUNNABLE");
lock.wait(1000); // 转为TIMED_WAITING
System.out.println("线程被唤醒,再次RUNNABLE");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
System.out.println("创建线程:" + thread.getState()); // NEW
thread.start();
Thread.sleep(50); // 确保thread进入wait
System.out.println("调用wait后:" + thread.getState()); // TIMED_WAITING
synchronized (lock) {
lock.notify(); // 唤醒thread
System.out.println("notify后:" + thread.getState()); // BLOCKED(因为主线程持有锁)
}
thread.join();
System.out.println("线程结束:" + thread.getState()); // TERMINATED
}
}
4. 常见问题排查技巧
4.1 线程卡在BLOCKED状态
典型表现:应用响应变慢,CPU使用率却不高。
排查步骤:
- 使用jstack获取线程dump
- 查找"BLOCKED (on object monitor)"的线程
- 分析锁持有者和等待链
bash复制jstack <pid> > thread_dump.txt
4.2 线程泄漏(大量WAITING/TIMED_WAITING)
常见原因:
- 线程池未正确关闭
- 忘记调用notify()/signal()
- 死锁导致所有工作线程挂起
检测工具:
- Java Mission Control
- VisualVM
- Arthas的thread命令
4.3 虚假唤醒问题
即使没有调用notify(),wait()也可能返回。这是底层操作系统特性导致的,正确的做法是在wait()返回后重新检查条件:
java复制synchronized(lock) {
while(!condition) { // 不要用if
lock.wait();
}
// 处理业务
}
5. 最佳实践与性能考量
-
减少锁竞争:
- 缩小同步代码块范围
- 使用读写锁(ReentrantReadWriteLock)替代独占锁
- 考虑无锁数据结构(如ConcurrentHashMap)
-
合理设置线程优先级:
java复制thread.setPriority(Thread.NORM_PRIORITY + 1);但要注意不同操作系统对优先级的处理方式不同。
-
使用线程池替代裸线程:
java复制ExecutorService executor = Executors.newFixedThreadPool(4); executor.submit(() -> {...}); -
监控线程状态:
java复制ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(true, true); -
处理未捕获异常:
java复制thread.setUncaughtExceptionHandler((t, e) -> { System.err.println("线程" + t.getName() + "抛出异常:" + e); });
理解线程生命周期不仅是为了应付面试,更是编写健壮并发程序的基础。在实际项目中,我经常使用jstack分析生产环境的线程状态,快速定位死锁或资源竞争问题。建议开发者多动手实践,通过实际案例加深对各个状态转换的理解。