1. 为什么我们需要区分 wait() 和 sleep()
刚接触Java多线程编程时,我也曾把wait()和sleep()混为一谈。直到有次线上事故让我付出了惨痛代价——一个本该在凌晨执行的定时任务,因为误用sleep()导致整个系统挂起8小时。这次教训让我明白:理解这两个方法的本质区别,是写出健壮多线程代码的基本功。
wait()和sleep()最根本的区别在于它们对锁的处理方式。当线程调用wait()时,它会释放持有的对象锁,允许其他线程进入同步块;而sleep()则不会释放任何锁资源,这可能导致严重的线程阻塞问题。举个例子,在生产者-消费者模型中,如果消费者线程误用sleep()而不是wait(),整个系统很快就会因为锁竞争而陷入死锁状态。
从使用场景来看,wait()通常用于线程间协作,比如等待某个条件满足;而sleep()更适合用于单纯的延时操作。在Android开发中,我曾见过有人用sleep()来等待网络请求返回,结果导致UI线程卡死——这种场景就应该用wait/notify机制配合异步回调。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从JVM层面看方法实现原理
2.1 wait()的底层机制
当调用wait()时,JVM会执行一系列精密操作:
- 首先检查当前线程是否持有对象锁,如果没有则抛出IllegalMonitorStateException
- 将线程加入该对象的等待集合(wait set)
- 原子性地释放对象锁并挂起线程
- 当被notify()唤醒后,线程需要重新获取对象锁才能继续执行
关键点在于第三步的"释放锁"操作。在数据库连接池的实现中,正是利用这个特性让等待连接的线程暂时释放资源,避免长时间占用锁导致性能下降。
2.2 sleep()的底层机制
sleep()的实现相对简单:
- 使当前线程暂停执行指定时间
- 不释放任何锁资源
- 唤醒后线程进入就绪状态
这里有个容易忽视的细节:sleep()的精度取决于操作系统调度器。在Windows系统上,实际休眠时间可能比指定的长15ms左右,这是由系统时钟中断周期决定的。我在开发高频交易系统时,就曾因为这个问题导致订单延迟,最终改用更精确的LockSupport.parkNanos()方法。
3. 使用场景对比与典型误区
3.1 正确使用wait()的场景
在电商系统开发中,订单超时处理是个经典案例:
java复制// 订单超时监控线程
synchronized(order) {
while(!order.isCompleted()) {
order.wait(TIMEOUT);
if(System.currentTimeMillis() - order.getCreateTime() > MAX_WAIT) {
cancelOrder(order);
break;
}
}
}
这里使用wait(timeout)而不是sleep()有几个关键优势:
- 可以在订单完成时被立即唤醒(通过notify)
- 等待期间释放锁,不影响其他线程操作订单数据
- 支持条件判断,避免虚假唤醒
3.2 sleep()的适用情况
在游戏服务器开发中,我们可能用sleep()来实现简单的战斗冷却:
java复制void attack() {
// 攻击逻辑...
try {
Thread.sleep(COOLDOWN_TIME); // 简单的冷却时间
} catch(InterruptedException e) {
Thread.currentThread().interrupt();
}
}
但要注意:这种用法仅限于单线程环境或不需要同步的简单场景。如果这段代码出现在synchronized块中,所有试图访问该锁的线程都会被阻塞。
3.3 常见反模式
我见过最危险的误用是在分布式锁中:
java复制while(!tryLock()) {
Thread.sleep(100); // 错误!应该用wait/notify
}
这种轮询方式会导致:
- 不必要的CPU资源浪费
- 响应延迟(最多要等sleep时间)
- 在高并发场景下可能引发"惊群效应"
4. 进阶话题:中断处理与性能影响
4.1 中断响应差异
两个方法对中断的响应方式不同:
- sleep()被中断时会清除中断状态并抛出InterruptedException
- wait()被中断后除了抛出异常,还会重新获取锁(可能再次阻塞)
这导致它们的异常处理模式也有区别。正确的做法是:
java复制// sleep的中断处理
try {
Thread.sleep(1000);
} catch(InterruptedException e) {
Thread.currentThread().interrupt(); // 恢复中断状态
// 清理资源...
}
// wait的中断处理
synchronized(lock) {
try {
lock.wait();
} catch(InterruptedException e) {
// 通常需要重新检查条件
if(!condition) continue;
}
}
4.2 性能考量
在百万级并发的消息系统中,我们做过基准测试:
- 使用wait/notify的方案QPS达到12万
- 使用sleep轮询的方案QPS不足3万
- 且后者CPU使用率高出40%
这是因为:
- sleep()不会释放锁,导致更多线程竞争
- 上下文切换次数显著增加
- 无法及时响应条件变化
5. 现代Java中的替代方案
虽然wait/notify是基础,但在新项目中我更推荐:
5.1 java.util.concurrent工具包
比如用CountDownLatch实现启动同步:
java复制CountDownLatch latch = new CountDownLatch(THREAD_COUNT);
// 工作线程
void run() {
// ...工作逻辑
latch.countDown();
}
// 主线程
latch.await(); // 比wait()更直观
5.2 CompletableFuture
对于异步编程,CompletableFuture提供了更强大的API:
java复制CompletableFuture.supplyAsync(() -> fetchData())
.thenApply(data -> process(data))
.thenAccept(result -> updateUI(result));
5.3 Lock/Condition接口
比synchronized更灵活的选择:
java复制Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
lock.lock();
try {
while(!conditionMet) {
condition.await(); // 类似wait()
}
// ...
} finally {
lock.unlock();
}
在实际项目中,我发现这些高级API不仅更安全,还能减少约30%的同步相关bug。特别是在使用线程池时,它们能更好地与Executor框架集成。
