Java线程的六种状态转换与sleep/wait/yield的区别
Java多线程编程中,线程状态的控制是核心难点。理解线程的生命周期以及常用方法的区别,能够帮助我们编写出更高效、更稳定的并发程序。
1. 理解Java线程的六种状态
Java线程的生命周期被明确定义在 Thread.State 枚举中。线程在任何时刻只能处于其中一种状态。
-
NEW(新建状态)
线程对象被创建但尚未调用start()方法。此时仅仅是在堆内存中分配了资源,还没与操作系统线程关联。 -
RUNNABLE(可运行状态)
调用start()后进入该状态。它包含了两种微观情况:正在运行(Running)和就绪(Ready)。Java层面将其视为“正在运行或等待CPU调度”。 -
BLOCKED(阻塞状态)
线程试图获取一个被其他线程持有的监视器锁(synchronized块)时,会进入此状态。在等待锁释放的过程中,它无法执行任何代码。 -
WAITING(无限等待状态)
线程在等待另一个线程执行特定动作。调用Object.wait()、Thread.join()或LockSupport.park()等无超时参数的方法会导致此状态。除非被显式唤醒,否则线程不会自动恢复。 -
TIMED_WAITING(计时等待状态)
与 WAITING 类似,但带有超时时间。调用Thread.sleep()、Object.wait(long timeout)或Thread.join(long millis)等带有时间参数的方法会进入此状态。时间一到或被唤醒,线程就会尝试恢复执行。 -
TERMINATED(终止状态)
线程执行完毕(run()方法正常退出)或因异常意外退出。
2. 线程状态转换全景图
状态之间的转换遵循特定的规则。下面的流程图清晰展示了线程如何在不同状态间流转,特别是与锁和时间相关的路径。
重点关注以下转换逻辑:
- 进入阻塞:线程试图进入
synchronized保护的临界区,但锁已被占用,必须等待锁释放。 - 释放CPU与锁:调用
wait()时,线程不仅释放CPU,还会释放持有的锁。 - 保持锁:调用
sleep()时,线程释放CPU但继续持有锁。
3. 核心方法深度解析:sleep、wait、yield
这三个方法最容易混淆,但它们的应用场景和底层机制完全不同。
3.1 Thread.sleep()
- 定义:这是一个静态方法,作用是让当前正在执行的线程暂停指定的时间。
- 锁机制:不释放锁。如果线程当前持有
synchronized锁,休眠期间其他线程依然无法进入该同步块。 - 醒来方式:时间结束自动醒来;或被其他线程调用
interrupt()打断并抛出InterruptedException。 - 使用场景:简单的暂停,模拟耗时操作,或让出CPU给高优先级任务片刻。
3.2 Object.wait()
- 定义:定义在
Object类中,必须配合synchronized关键字使用(即必须在持有对象监视器锁的情况下调用)。 - 锁机制:释放锁。这是它和
sleep最大的区别。调用后,线程进入 WAITING 状态,直到被notify()或notifyAll()唤醒。 - 醒来方式:被其他线程通知;或等待超时(如果是带参的
wait(long))。 - 使用场景:线程间通信。例如生产者-消费者模型,生产满了就
wait,让消费者消费。
3.3 Thread.yield()
- 定义:静态方法,提示调度器当前线程愿意放弃当前使用的CPU。
- 锁机制:不释放锁。
- 执行效果:仅仅是一个建议。调度器可能会忽略这个提示,也可能只是将线程从“运行”移回“就绪”队列,随后立刻又重新选中它。它不保证一定会发生上下文切换。
- 使用场景:极少使用,仅在试图优化线程竞争或调试时辅助分析。
4. 方法对比总结表
为了更直观地展示区别,请参考下表。
| 特性 | Thread.sleep() |
Object.wait() |
Thread.yield() |
|---|---|---|---|
| 所属类 | Thread |
Object |
Thread |
| 锁释放 | 不释放 (Holds lock) | 释放 (Releases lock) | 不释放 (Holds lock) |
| 唤醒条件 | 超时或 interrupt |
notify/notifyAll 或超时 |
取决于调度器 (随时可能再次运行) |
| 使用位置 | 任何位置 | 必须在 synchronized 块/方法内 |
任何位置 |
| 异常处理 | 必须捕获 InterruptedException |
必须捕获 InterruptedException |
无需捕获异常 |
5. 代码实操:验证 sleep 与 wait 的锁行为
通过实际代码观察锁的释放情况。
编写测试类 LockBehaviorDemo:
public class LockBehaviorDemo {
// 共享锁对象
private static final Object lock = new Object();
public static void main(String[] args) {
// 线程1:调用 sleep
new Thread(() -> testSleep(), "Thread-Sleep").start();
// 线程2:调用 wait
new Thread(() -> testWait(), "Thread-Wait").start();
// 主线程稍作等待后,尝试唤醒 Thread-Wait
try {
Thread.sleep(1500);
synchronized (lock) {
System.out.println("Main thread: 准备唤醒 Thread-Wait...");
lock.notify();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void testSleep() {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + ": 获得锁,开始休眠3秒");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": 休眠结束,释放锁");
}
}
public static void testWait() {
try {
// 先让线程1先抢到锁
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + ": 尝试获取锁...");
System.out.println(Thread.currentThread().getName() + ": 获得锁,开始 wait");
try {
lock.wait(); // 释放锁并等待
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": 被唤醒,继续执行,即将释放锁");
}
}
}
运行上述代码并观察控制台输出。
预期结果分析:
Thread-Sleep先抢到锁并打印日志,随后开始sleep(3000)。- 虽然
Thread-Sleep正在休眠,但它没有释放锁。因此,Thread-Wait在尝试获取锁时会被BLOCKED,无法进入synchronized块。 - 直到 3秒后
Thread-Sleep执行完毕退出同步块,Thread-Wait才会获得锁并打印“获得锁,开始 wait”。 Thread-Wait调用wait()后,立即释放锁。- 主线程在 1.5秒时尝试获取锁进行
notify,此时可以成功进入,因为Thread-Wait已经释放了锁。
6. 关键步骤总结
在实际开发中处理线程状态时,请遵循以下最佳实践步骤:
-
选择正确的暂停方法:
- 如果仅需要暂停且不关心锁释放,使用
Thread.sleep()。 - 如果需要等待特定条件且允许其他线程介入,必须使用
wait()和notify()机制。
- 如果仅需要暂停且不关心锁释放,使用
-
处理中断异常:
- 调用
sleep()或wait()时,必须在try-catch块中捕获InterruptedException,并根据业务逻辑决定是终止线程还是恢复运行。
- 调用
-
避免滥用 yield:
- 除非你对 JVM 的线程调度机制有极深的理解,否则不要使用
yield(),它往往会导致性能下降且难以调试。
- 除非你对 JVM 的线程调度机制有极深的理解,否则不要使用
-
检查同步上下文:
- 调用
wait()前,务必确认当前线程持有该对象的监视器锁,否则会抛出IllegalMonitorStateException。
- 调用

暂无评论,快来抢沙发吧!