1. 线程等待与通知机制概述
在并发编程中,线程间的协调是保证程序正确性的关键。当多个线程需要按照特定顺序执行或共享资源时,单纯的锁机制往往不够高效。Java内置的等待/通知机制(wait/notify)提供了一种线程间通信的优雅解决方案,它允许线程在满足特定条件前主动释放锁并进入等待状态,避免无意义的CPU空转。
这个机制的核心思想源自操作系统的管程(Monitor)概念。每个Java对象都内置了一个等待集(wait set),当线程调用wait()方法时,它会释放对象锁并进入该集合休眠,直到其他线程调用notify()或notifyAll()将其唤醒。这种设计完美契合了生产者-消费者等经典并发模式的需求。
关键理解:wait()调用会立即释放锁,这与sleep()有本质区别。sleep()不会释放任何锁资源,线程会继续保持所有已获得的锁。
2. 核心API原理解析
2.1 等待方法wait()
wait()方法有三个重载版本:
- wait(): 无限期等待,直到被中断或唤醒
- wait(long timeout): 等待指定毫秒数
- wait(long timeout, int nanos): 更高精度的超时控制
public final void wait() throws InterruptedException { wait(0); }当线程调用wait()时,JVM会执行以下原子操作:
- 释放对象关联的监视器锁
- 将线程状态改为WAITING
- 将线程加入对象的等待集
- 等待直到被通知、中断或超时
重要细节:wait()必须在同步块内调用,否则会抛出IllegalMonitorStateException。这是因为线程必须持有锁才能释放它。
2.2 通知方法notify()与notifyAll()
notify()随机唤醒等待集中的一个线程,而notifyAll()会唤醒所有等待线程。被唤醒的线程需要重新竞争对象锁才能继续执行。
public final native void notify(); public final native void notifyAll();选择使用哪个通知方法取决于业务场景:
- notify():效率更高,但可能产生"线程饥饿"
- notifyAll():更公平,但会引发"惊群效应"
3. 标准使用范式与实现示例
3.1 经典模板代码
正确的等待/通知模式应遵循以下结构:
// 等待方 synchronized(lock) { while(!condition) { // 必须用while而不是if lock.wait(); } // 处理业务逻辑 } // 通知方 synchronized(lock) { condition = true; lock.notifyAll(); }3.2 生产者-消费者实战
下面是一个容量受限的队列实现:
class BoundedQueue<T> { private final Object[] items; private int putPtr, takePtr, count; private final Object lock = new Object(); public BoundedQueue(int capacity) { items = new Object[capacity]; } public void put(T x) throws InterruptedException { synchronized(lock) { while(count == items.length) { lock.wait(); // 队列满时等待 } items[putPtr] = x; if(++putPtr == items.length) putPtr = 0; ++count; lock.notifyAll(); // 唤醒可能等待的消费者 } } public T take() throws InterruptedException { synchronized(lock) { while(count == 0) { lock.wait(); // 队列空时等待 } @SuppressWarnings("unchecked") T x = (T) items[takePtr]; if(++takePtr == items.length) takePtr = 0; --count; lock.notifyAll(); // 唤醒可能等待的生产者 return x; } } }4. 高级应用与性能优化
4.1 条件队列的选择策略
对于复杂条件谓词,可以使用多个条件队列提高效率。Java的Condition接口提供了更灵活的实现:
class ResourcePool { private final Lock lock = new ReentrantLock(); private final Condition notEmpty = lock.newCondition(); private final Condition notFull = lock.newCondition(); public void get() throws InterruptedException { lock.lock(); try { while(isEmpty()) { notEmpty.await(); } // 获取资源 notFull.signal(); } finally { lock.unlock(); } } }4.2 虚假唤醒的防御
虚假唤醒(Spurious Wakeup)是指线程在没有收到明确通知的情况下被唤醒。防御措施包括:
- 总是使用while循环检查条件谓词
- 记录唤醒原因(可通过自定义异常实现)
- 添加唤醒日志便于问题追踪
5. 常见陷阱与最佳实践
5.1 典型错误模式
丢失唤醒(Missed Signal):
- 错误:先修改条件变量再调用notify()
- 正确:在同步块内先修改条件再通知
条件检查不完整:
- 错误:使用if而不是while检查条件
- 正确:所有条件检查都使用while循环
锁泄露:
- 错误:wait()前未释放其他资源锁
- 正确:确保wait()时只持有单一锁
5.2 性能优化技巧
通知范围最小化:
- 只在确实改变条件谓词时调用notify()
- 优先使用notify()而非notifyAll()
超时控制:
synchronized(lock) { long remaining = TIMEOUT; long end = System.nanoTime() + remaining; while(!condition && remaining > 0) { TimeUnit.NANOSECONDS.timedWait(lock, remaining); remaining = end - System.nanoTime(); } }对象监视器选择:
- 避免使用业务对象作为锁
- 推荐使用专用锁对象:
private final Object lock = new Object(); // 专用监视器
6. 现代替代方案比较
6.1 java.util.concurrent工具包
- BlockingQueue:内置等待/通知机制的线程安全队列
- CountDownLatch:一次性等待多个线程完成
- CyclicBarrier:可重复使用的线程栅栏
- Semaphore:控制资源访问的计数信号量
6.2 响应式编程方案
Reactor和RxJava等框架通过事件驱动模型避免显式线程同步:
Flux.interval(Duration.ofMillis(100)) .take(10) .subscribe(System.out::println);7. 调试与问题诊断
7.1 线程转储分析
使用jstack或VisualVM获取线程转储,重点关注:
- 处于WAITING状态的线程及其持有的锁
- 锁的拥有者线程
- 死锁环(deadlock cycle)
7.2 日志增强技巧
添加详细的等待/通知日志:
class DebuggableLock { void waitWithLog(String reason) throws InterruptedException { log.debug("Thread {} waiting because: {}", Thread.currentThread(), reason); wait(); log.debug("Thread {} awakened", Thread.currentThread()); } }在实际项目中,我发现合理使用等待/通知机制可以将某些场景的性能提升3-5倍。但必须注意,过度使用会导致代码复杂度急剧上升。对于现代Java开发,优先考虑java.util.concurrent中的高级工具通常是更安全的选择。当确实需要直接使用wait/notify时,务必严格遵循标准范式,并添加详尽的文档说明。