前言
在Java多线程编程中,理解线程的生命周期和状态是开发高效并发程序的基础。线程作为操作系统调度的基本单位,其状态变化直接影响程序的执行效率和资源管理。本文将深入解析Java线程的生命周期、六种核心状态及其转换规则,并结合代码示例帮助读者掌握实际应用技巧。
一、Java线程的生命周期
Java线程的生命周期从创建到终止,经历多个状态转换。在JDK 1.5及之后版本中,线程状态由java.lang.Thread.State枚举类定义,共包含以下6种状态:
NEW(新建)
RUNNABLE(可运行)
BLOCKED(阻塞)
WAITING(等待)
TIMED_WAITING(计时等待)
TERMINATED(终止)
二、线程的6种状态详解
1. NEW(新建)
-
定义:线程对象被创建但尚未启动。
-
触发条件:通过
new Thread()创建线程对象,但未调用start()方法。 -
特点:
- 线程未与操作系统线程关联,仅是一个Java对象实例。
- 无法执行任何代码,直到调用
start()。
-
示例:
Thread thread = new Thread(() -> System.out.println("线程运行")); System.out.println(thread.getState()); // 输出: NEW
2. RUNNABLE(可运行)
-
定义:线程已启动,等待CPU调度运行。
-
触发条件:调用
start()方法后。 -
特点:
-
包含两种子状态:就绪(Ready) 和 运行(Running) 。
- 就绪:线程准备运行,等待CPU时间片分配。
- 运行:线程获得CPU时间片,正在执行代码。
-
-
示例:
thread.start(); System.out.println(thread.getState()); // 可能输出: RUNNABLE
public class MultiThreadDemo {
public static void main(String[] args) {
// 创建一个新的线程,并通过 Lambda 表达式定义线程的任务
Thread thread = new Thread(() -> System.out.println("线程运行"));
// 启动线程,线程进入“可运行”状态
thread.start();
// 输出线程当前的状态,此时线程已经处于“可运行”状态
System.out.println(thread.getState());
}
}
输出结果:
RUNNABLE
线程运行
为什么先执行System.out.println(thread.getState());,后执行线程任务?
在这段代码中,System.out.println(thread.getState()); 和 thread.start() 是在主线程中顺序执行的,但由于线程的调度和执行是由操作系统管理的,主线程和新创建的子线程是并发执行的。因此,thread.getState() 打印的值是主线程调用 getState() 时该线程的状态,而此时线程的状态通常已经是 RUNNABLE,但实际上它的任务(System.out.println("线程运行"))可能还没有执行。
解释原因:
-
start() 调用并不会立刻执行线程的任务:start() 方法只是将线程标记为“可运行”,并且线程会进入“就绪”状态,等待操作系统调度执行。而不是立即开始执行。- 线程的实际执行时间完全由操作系统的线程调度机制来控制。可能会发生新线程被调度到执行队列中的时候,主线程就已经执行完了
System.out.println(thread.getState())。
-
主线程与子线程是并发执行的:
- 主线程在执行
thread.start() 后,立即继续执行下一行System.out.println(thread.getState()),并且此时子线程可能已经进入“就绪”状态(RUNNABLE),但还没有开始执行任务。 - 线程调度有时会有延迟,因此你可能会看到“RUNNABLE”状态先被输出,再看到“线程运行”被打印出来。这个顺序的关键在于操作系统如何调度线程。
- 主线程在执行
-
线程的状态(
RUNNABLE ) :- 线程一旦被启动,通常进入
RUNNABLE 状态(意味着它已经准备好执行)。然而,实际执行任务的时间点是由线程调度器决定的,主线程调用getState() 时,线程很可能已经进入了RUNNABLE 状态,但尚未开始打印"线程运行"。
- 线程一旦被启动,通常进入
简单总结:
System.out.println(thread.getState()) 在主线程中执行时,打印的是线程的当前状态,通常是RUNNABLE,而子线程可能还没有得到 CPU 时间片来执行任务。- 主线程和子线程是并行执行的,线程调度的顺序可能导致打印顺序看起来和你预期的不一样。
3. BLOCKED(阻塞)
-
定义:线程因等待锁而被阻塞。
-
触发条件:尝试获取一个被其他线程持有的锁(如
synchronized锁)。 -
特点:
- 需等待锁释放后,线程才能重新进入RUNNABLE状态。
-
示例:
public class MultiThreadDemo { public static void main(String[] args) throws InterruptedException { // 创建一个专用锁对象,用于线程同步 // 使用 Object 作为锁是 Java 的常见做法,确保线程安全 // 作用:控制多个线程对共享资源的访问,避免数据竞争 Object lock = new Object(); // 线程 t1:先获取锁并睡眠 Thread t1 = new Thread(() -> { synchronized (lock) { System.out.println(Thread.currentThread().getName() + " 正在持有锁"); try { Thread.sleep(2000); // 持有锁 2 秒 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " 释放锁"); } }, "Thread-t1"); // 线程 t2:尝试获取锁,进入 BLOCKED 状态 Thread t2 = new Thread(() -> { synchronized (lock) { System.out.println(Thread.currentThread().getName() + " 获取到锁"); } }, "Thread-t2"); // 启动线程 t1.start(); // 等待 t1 进入 RUNNABLE 并获取锁 Thread.sleep(100); System.out.println("t1 当前状态: " + t1.getState()); System.out.println("t2 当前状态: " + t2.getState()); t2.start(); // 等待 t2 尝试获取锁并进入 BLOCKED 状态 Thread.sleep(200); System.out.println("t1 当前状态: " + t1.getState()); System.out.println("t2 当前状态: " + t2.getState()); // 等待 t1 释放锁,t2 进入 RUNNABLE 并执行 Thread.sleep(3000); System.out.println("t1 当前状态: " + t1.getState()); System.out.println("t2 当前状态: " + t2.getState()); // 等待所有线程结束 t1.join(); t2.join(); System.out.println("最终状态:"); System.out.println("t1 最终状态: " + t1.getState()); System.out.println("t2 最终状态: " + t2.getState()); } }
4. WAITING(等待)
-
定义:线程主动进入无限期等待,需其他线程显式唤醒。
-
触发条件:
- 调用
Object.wait()、Thread.join()或LockSupport.park()。
- 调用
-
特点:
- 需通过
notify()、notifyAll()或interrupt()唤醒。
- 需通过
-
示例:
public class WaitingExample { public static void main(String[] args) { Object lock = new Object(); // 创建线程t1,进入等待状态 Thread t1 = new Thread(() -> { synchronized (lock) { try { System.out.println("t1进入等待状态"); lock.wait(); // 进入WAITING状态 System.out.println("t1被唤醒"); } catch (InterruptedException e) { e.printStackTrace(); } } }); t1.start(); try { // 主线程让出CPU,确保t1先执行 Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } // 创建线程t2,用于唤醒t1 Thread t2 = new Thread(() -> { synchronized (lock) { System.out.println("t2唤醒t1"); lock.notify(); // 唤醒t1 } }); t2.start(); } }
5. TIMED_WAITING(计时等待)
-
定义:线程进入有限期等待,超时后自动恢复。
-
触发条件:
- 调用
Thread.sleep(long)、Object.wait(long)、Thread.join(long)等带超时参数的方法。
- 调用
-
特点:
- 超时后自动返回RUNNABLE状态,或被其他线程唤醒。
-
示例:
public class TimedWaitingExample { public static void main(String[] args) throws InterruptedException { // 线程t1:计时等待 Thread t1 = new Thread(() -> { try { System.out.println("t1: 开始计时等待(2秒)"); Thread.sleep(2000); // 进入TIMED_WAITING状态 System.out.println("t1: 计时结束,自动恢复"); } catch (InterruptedException e) { e.printStackTrace(); } }); t1.start(); Thread.sleep(500); // 主线程短暂休眠,确保t1进入TIMED_WAITING // 展示t1的状态(实际开发中不需要频繁轮询) System.out.println("t1状态: " + t1.getState()); // 输出 TIMED_WAITING t1.join(); // 主线程等待t1执行完毕 System.out.println("t1状态: " + t1.getState()); // 输出 TERMINATED } }
6. TERMINATED(终止)
-
定义:线程运行结束或因异常退出。
-
触发条件:
run()方法执行完毕。- 线程因未捕获异常终止。
-
特点:
- 终止后不可再次启动。
-
示例:
Thread t1 = new Thread(() -> System.out.println("线程结束")); t1.start(); try { t1.join(); // 等待线程结束 } catch (InterruptedException e) {} System.out.println(t1.getState()); // 输出: TERMINATED
三、线程生命周期的控制方法
在Java中,线程的生命周期由多种方法控制。以下是一些常用的方法及其作用:
1 start()
- 启动线程并使线程进入“可运行状态”。
Thread thread = new Thread(() -> {
// 执行任务
});
thread.start(); // 启动线程
2 sleep()
- 使当前线程进入“超时等待状态”一段时间。
Thread.sleep(1000); // 当前线程休眠 1 秒
3 join()
- 当前线程调用另一个线程的
join() 方法时,当前线程会等待被调用线程完成后才继续执行。
Thread thread1 = new Thread(() -> {
// 执行任务
});
Thread thread2 = new Thread(() -> {
// 执行任务
});
thread1.start();
thread2.start();
thread1.join(); // 等待 thread1 执行完毕
4 wait()、notify()、notifyAll()
wait() 使当前线程进入等待状态。notify() 唤醒一个正在等待的线程。notifyAll() 唤醒所有等待的线程。
synchronized (lock) {
lock.wait(); // 当前线程进入等待状态
lock.notify(); // 唤醒一个等待线程
}
5. 线程的中断
线程的中断操作是通过 Thread.interrupt() 方法实现的。它不会直接终止线程,而是给线程发送一个中断信号,线程可以选择通过捕获中断异常来停止执行。
Thread thread = new Thread(() -> {
while (!Thread.interrupted()) {
// 执行任务
}
});
thread.start();
thread.interrupt(); // 发出中断信号
四 、状态转换图
NEW
↓ start()
RUNNABLE ←→ BLOCKED/WAITING/TIMED_WAITING
↑ wait(), join(), sleep() ↓ notify(), notifyAll(), interrupt(), timeout
TERMINATED ← [run()执行完毕或异常]
五 、关键状态区别与注意事项
1. RUNNABLE vs RUNNING
- RUNNABLE:包含就绪和运行两种状态,由JVM统一表示。
- RUNNING:线程实际占用CPU执行代码,但Java不单独定义此状态。
2. BLOCKED vs WAITING
| 状态 | 触发条件 | 资源释放 |
|---|---|---|
| BLOCKED | 竞争锁失败(如synchronized) | 不释放当前锁 |
| WAITING | 主动调用wait()、join()等方法 | 释放所有锁 |
3. 锁竞争与死锁
- 锁竞争:多个线程争夺锁资源时,未成功获取的线程进入BLOCKED状态。
- 死锁:线程A持有锁1并等待锁2,线程B持有锁2并等待锁1,导致双方永久阻塞。
六、总结
掌握Java线程的生命周期和状态是编写高效并发程序的关键。通过理解以下要点,开发者可以避免常见问题(如死锁、资源竞争),并优化程序性能:
- 状态转换规则:从NEW到TERMINATED的完整流程。
- 锁与等待的区别:BLOCKED与WAITING的触发条件和资源释放差异。
- 调试工具:使用
jstack、VisualVM分析线程状态,定位性能瓶颈。
“线程生命周期与状态的掌握,是每个 Java 程序员迈向高阶的重要一步。今天的文章只是为你揭开了这神秘领域的序幕。更多精彩深入的案例分析、实战技巧以及更多 Java 进阶知识,尽在我们的公众号 [技海拾贝]。