1. 线程控制基础:sleep()与wait()的本质区别
在Java多线程编程中,sleep()和wait()是两个最容易被混淆的线程控制方法。很多初级开发者在使用时常常搞不清楚它们的适用场景,导致出现线程死锁或非预期的线程行为。作为在Java并发领域踩过无数坑的老手,我想通过这篇技术笔记彻底讲清楚这两个方法的区别。
首先从最根本的定位来看:sleep()是Thread类的静态方法,它的作用很简单——让当前执行的线程暂停指定的时间。而wait()是Object类的实例方法,它设计出来就是为了实现线程间的协作通信。这个根本定位的不同,直接导致了它们在锁处理、使用场景等各方面的差异。
关键记忆点:sleep()是线程自私的行为,wait()是线程无私的协作
2. 锁行为对比:抱着锁睡觉 vs 放开锁等待
2.1 sleep()的锁行为
当线程调用sleep()时,它进入的是TIMED_WAITING状态,但关键点是:它不会释放已经持有的任何锁资源。这就好比一个人抱着钥匙睡着了,其他需要这把钥匙的人只能干等着。
java复制synchronized(lock) {
System.out.println("持有锁进入睡眠");
Thread.sleep(2000); // 睡眠期间锁仍然被当前线程持有
System.out.println("醒来后继续持有锁");
}
这种特性使得sleep()非常适合用在不需要与其他线程交互的独立任务中,比如:
- 模拟网络请求间的延迟
- 控制轮询检查的频率
- 实现简单的定时任务
2.2 wait()的锁行为
相比之下,wait()调用后线程会立即释放它持有的锁,进入WAITING状态。这就像一个人把钥匙交给管理员,自己到休息室等待通知。这种机制是Java线程间通信的基础。
java复制synchronized(lock) {
while(!condition) {
lock.wait(); // 立即释放lock,进入等待
}
// 被唤醒后重新获得锁
}
这种锁释放的特性使得wait()非常适合:
- 生产者-消费者模型
- 工作线程池的任务分配
- 任何需要条件触发的场景
3. 使用约束与常见陷阱
3.1 sleep()的自由与限制
sleep()可以在任何地方调用,没有特别的约束条件。但需要注意:
- 它只影响当前线程,不会干扰其他线程
- 睡眠时间到了会自动唤醒(除非被interrupt)
- 睡眠期间CPU不会执行该线程
常见错误是误以为sleep()会让出CPU给所有线程,实际上它只是让出当前线程的CPU时间片。
3.2 wait()的严格约束
wait()必须在同步代码块中使用,这个约束很多新手会忽略:
java复制// 错误示范 - 会抛出IllegalMonitorStateException
public void wrongMethod() {
lock.wait(); // 没有在synchronized块中
}
// 正确用法
public void correctMethod() {
synchronized(lock) {
lock.wait(); // 合法调用
}
}
这是因为wait()需要操作对象的monitor锁,而只有持有锁的线程才能释放它。这个设计确保了线程安全。
4. 唤醒机制深度解析
4.1 sleep()的自动唤醒
sleep()的唤醒完全由时间控制:
java复制Thread.sleep(1000); // 精确睡眠1秒(实际可能有几毫秒误差)
唤醒后线程进入就绪状态,等待CPU调度。
4.2 wait()的复杂唤醒机制
wait()的唤醒要复杂得多,有三种方式:
- 其他线程调用
notify()/notifyAll() - 设置了超时时间自动唤醒
- 线程被interrupt
特别注意虚假唤醒问题(spurious wakeup),这是很多bug的根源。正确的做法总是用循环检查条件:
java复制synchronized(lock) {
while(!condition) { // 必须用while而不是if
lock.wait();
}
// 处理业务逻辑
}
5. 实战场景对比分析
5.1 适合sleep()的场景
定时任务执行器:
java复制public class TimerTask implements Runnable {
@Override
public void run() {
while(true) {
doTask();
try {
Thread.sleep(60000); // 每分钟执行一次
} catch (InterruptedException e) {
break;
}
}
}
}
模拟网络延迟:
java复制public class MockNetwork {
public Response sendRequest(Request req) {
try {
Thread.sleep(200); // 模拟网络延迟
} catch (InterruptedException ignored) {}
return realSend(req);
}
}
5.2 适合wait()的场景
生产者-消费者模型:
java复制class Buffer {
private Queue<Integer> queue = new LinkedList<>();
private int capacity;
public synchronized void produce(int item) throws InterruptedException {
while(queue.size() == capacity) {
wait(); // 缓冲区满时等待
}
queue.add(item);
notifyAll(); // 通知消费者
}
public synchronized int consume() throws InterruptedException {
while(queue.isEmpty()) {
wait(); // 缓冲区空时等待
}
int item = queue.remove();
notifyAll(); // 通知生产者
return item;
}
}
多阶段任务处理:
java复制class PhaseController {
private int currentPhase = 0;
public synchronized void completePhase() {
currentPhase++;
notifyAll(); // 通知等待下一阶段的线程
}
public synchronized void waitForPhase(int targetPhase) throws InterruptedException {
while(currentPhase < targetPhase) {
wait(); // 等待目标阶段
}
}
}
6. 性能考量与最佳实践
6.1 sleep()的精度问题
sleep()的睡眠时间并不精确,受系统调度影响。对于需要高精度定时的场景,应该考虑:
ScheduledExecutorServiceLockSupport.parkNanos()- 专门的定时器库
6.2 wait()的性能优化
大量线程在同一个锁上wait()/notifyAll()会导致"惊群效应"。优化方案:
- 使用多个条件变量(Condition)
- 考虑使用更高级的并发工具:
CountDownLatchCyclicBarrierPhaser
7. 常见问题排查指南
7.1 为什么我的wait()抛出IllegalMonitorStateException?
99%的情况是因为:
- 没有在同步块中调用wait()
- 同步的对象与wait()的对象不一致
java复制// 错误示例1
public void method() {
synchronized(lock1) {
lock2.wait(); // 锁对象不匹配
}
}
// 错误示例2
public void method() {
lock.wait(); // 没有同步块
}
7.2 sleep()和wait()都会抛出InterruptedException
这是Java线程中断机制的一部分。正确处理方式:
java复制try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// 恢复中断状态(重要!)
Thread.currentThread().interrupt();
// 执行清理工作后退出
return;
}
7.3 为什么wait()要在循环中调用?
因为:
- 防止虚假唤醒
- 条件可能在被通知后再次变为false
- 多个线程可能共享同一个条件
8. 高级技巧与经验分享
8.1 使用wait(timeout)实现灵活控制
结合超时的wait()可以避免永久等待:
java复制synchronized(lock) {
long remaining = timeout;
long end = System.currentTimeMillis() + timeout;
while(!condition && remaining > 0) {
lock.wait(remaining);
remaining = end - System.currentTimeMillis();
}
}
8.2 避免嵌套锁中的wait()
在持有多个锁时调用wait()很容易导致死锁:
java复制// 危险代码!
synchronized(lock1) {
synchronized(lock2) {
lock1.wait(); // 只释放lock1,仍持有lock2
}
}
8.3 替代方案:并发工具类
现代Java开发中,很多场景可以用更高级的工具替代wait/notify:
BlockingQueue- 替代生产者消费者Semaphore- 控制资源访问Future/CompletableFuture- 异步结果处理
9. 从JVM角度看实现原理
9.1 sleep()的底层实现
sleep()最终会调用native方法:
java复制// Thread.java
public static native void sleep(long millis) throws InterruptedException;
在HotSpot JVM中,这会:
- 设置线程状态为TIMED_WAITING
- 通过操作系统调度器挂起线程
- 设置定时器,到期后重新调度
9.2 wait()的监视器机制
wait()涉及Java对象监视器(monitor)的复杂操作:
- 将线程加入对象的等待集
- 释放对象锁
- 将线程状态改为WAITING
- 被notify后重新竞争锁
10. 历史演变与版本变化
从Java早期版本到现在,这些方法的行为基本保持一致,但周边生态发生了变化:
- Java 5引入了更丰富的并发工具
- Java 7改进了锁的实现
- Java 9开始优化了线程调度
但核心的sleep/wait机制保持稳定,因为它们是Java并发模型的基础。