在Java多线程编程中,会话管理是一个典型的高并发场景:多个客户端线程不断创建、获取和释放会话资源,而会话本身数量有限,必须依靠同步机制协调各线程的行为。wait和notify正是JDK提供的最基础的线程间协作工具,但它们的使用规则相当严格,稍有疏忽就会引发死锁、虚假唤醒或非法监视器状态异常。本文将从原理到实践,完整剖析这套等待通知机制。

一、wait/notify的底层工作原理
wait和notify定义在java.lang.Object类中,这意味着任何一个Java对象都可以作为同步锁和等待队列的载体。每个对象内部都关联着一个监视器(monitor),其中维护了两个关键结构:一个是入口队列,存放竞争锁的线程;另一个是等待队列,存放调用了wait方法的线程。
当线程在某个对象上调用wait()时,JVM会执行三个动作:第一,释放该对象持有的监视器锁;第二,把当前线程放入对象的等待集(wait set)并置为阻塞状态;第三,被唤醒后重新参与锁竞争,只有重新拿到锁才能从wait调用处返回。这个第三点非常关键,很多初学者以为notify之后线程立即恢复执行,实际上被唤醒的线程还需要重新排队获取锁。
notify()方法的作用是从等待集中随机挑选一个线程唤醒,而notifyAll()会唤醒等待集中的全部线程。被唤醒的线程并不会立刻运行,而是进入入口队列等待获取锁。正因如此,notifyAll之后往往只有拿到锁的那个线程能继续往下走,其余线程如果发现条件仍不满足,会再次调用wait进入等待状态,形成经典的条件轮询模式。
还有一个重要的规则:wait和notify必须在持有对应对象监视器锁的前提下调用,否则会抛出IllegalMonitorStateException。看一个错误示例:
public class WrongDemo {
private final Object lock = new Object();
public void consumer() {
// 错误:没有持有lock的监视器锁就直接调用wait
lock.wait(); // 抛出 IllegalMonitorStateException
}
}
正确做法是先通过synchronized获取锁,再在同步块内调用wait。这背后的设计原因是:检查共享条件本身就需要锁保护,否则会在检查条件与进入等待之间产生竞态窗口,导致通知丢失。
二、多线程会话管理的完整实战
下面用一个会话池的例子来演示等待通知机制的典型应用。假设系统维护一个固定容量的会话队列,处理线程从队列中取会话进行业务处理,网络线程不断把新会话放入队列。当队列空时处理线程等待,队列满时网络线程等待,这本质上就是生产者消费者模型。
import java.util.LinkedList;
import java.util.Queue;
public class SessionPool {
private final Queue<String> sessions = new LinkedList<>();
private final int capacity;
private final Object notEmpty = new Object();
public SessionPool(int capacity) {
this.capacity = capacity;
}
// 生产者:网络线程放入新会话
public void offer(String sessionId) throws InterruptedException {
synchronized (notEmpty) {
// 必须用while而不是if,防止虚假唤醒
while (sessions.size() == capacity) {
notEmpty.wait(); // 队列已满,等待消费者释放空间
}
sessions.offer(sessionId);
notEmpty.notifyAll(); // 唤醒等待的消费者
}
}
// 消费者:工作线程取出会话处理
public String take() throws InterruptedException {
synchronized (notEmpty) {
while (sessions.isEmpty()) {
notEmpty.wait(); // 队列为空,等待生产者投放
}
String session = sessions.poll();
notEmpty.notifyAll(); // 唤醒等待的生产者
return session;
}
}
}
上面代码中有两个细节值得反复强调。第一个是while循环的使用。JDK官方文档明确指出,线程可能在没有被notify的情况下“虚假唤醒”(spurious wakeup),也可能在notify发出后、重新拿到锁之前条件又被其他线程改变了。因此醒来后必须重新检查条件,用if判断一次就继续执行的做法存在严重隐患。
第二个细节是notifyAll与notify的选择。notify只随机唤醒一个线程,如果等待队列中既有生产者又有消费者,notify可能唤醒的是同类型线程,导致真正需要执行的线程继续沉睡,这就是所谓的信号丢失问题。notifyAll虽然会带来一定的锁竞争开销,但在正确性上更稳妥。一般原则是:除非能确定所有等待线程都在等同一个条件,否则优先使用notifyAll。
另外注意wait()与wait(long timeout)的区别。无参wait会无限期等待,而带超时参数的版本在等待指定毫秒后自行返回,适合实现会话超时自动回收这类功能。Thread.interrupted会抛出InterruptedException,因此生产环境中不要吞掉这个异常,应该捕获后恢复中断标记或者向上传播,让上层框架有机会感知线程的终止请求。
三、常见陷阱与Condition替代方案
使用wait/notify时最常见的问题有三个。一是锁对象不一致:在一个对象上加锁,却在另一个对象上调用wait,两个线程看似同步实则毫无关联。二是唤醒顺序错误:先释放锁再notify的代码顺序虽然不会编译报错,但如果notify发生在消费者进入等待之前,通知就永远丢失了,消费者将陷入无限等待。三是死锁:多个线程以不同顺序获取多把锁,或线程持有锁的同时等待自己释放锁的条件。
排查这类问题可以使用jstack工具导出线程快照,观察BLOCKED和WAITING状态的线程及其持有的锁信息。在代码层面,尽量将锁、条件判断和状态修改收敛到同一个类中,减少跨对象协作。
从JDK 5开始,java.util.concurrent.locks包提供了Condition接口作为wait/notify的现代化替代品。它的核心优势在于一个锁可以挂多个等待队列,而synchronized对象的等待集只有一个。对于会话池这种既有“非空”又有“非满”两个条件的场景,两个Condition能让通知精确命中目标线程,避免无意义的批量唤醒。
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ConditionSessionPool {
private final Queue<String> sessions = new LinkedList<>();
private final int capacity;
private final Lock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
public void offer(String sessionId) throws InterruptedException {
lock.lockInterruptibly();
try {
while (sessions.size() == capacity) {
notFull.await(); // 只等待非满条件
}
sessions.offer(sessionId);
notEmpty.signal(); // 只唤醒等待非空条件的线程
} finally {
lock.unlock();
}
}
public String take() throws InterruptedException {
lock.lockInterruptibly();
try {
while (sessions.isEmpty()) {
notEmpty.await();
}
String session = sessions.poll();
notFull.signal();
return session;
} finally {
lock.unlock();
}
}
}
Condition的await对应wait,signal对应notify,signalAll对应notifyAll,语义几乎一致但功能更丰富,例如支持超时到纳秒精度的awaitNanos、可不响应中断的awaitUninterruptibly等。更重要的是,ReentrantLock还支持公平锁模式,让等待最久的线程优先获得锁,这对会话处理顺序敏感的业务很有价值。
实际工程中,如果不需要如此精细的控制,直接使用现成的LinkedBlockingQueue就是最佳选择,它内部正是基于两把锁和两个Condition实现的高性能阻塞队列。理解wait/notify的意义在于掌握底层协作原理,无论日后使用线程池、消息队列还是分布式锁,这套条件等待的思维方式都是通用的。
总结一下要点:wait必须配合synchronized使用并在while循环中检查条件;优先考虑notifyAll保证唤醒的正确性;条件复杂时用Condition拆分等待队列;始终正确处理InterruptedException。掌握这些规则,多线程会话管理的同步问题就能迎刃而解。
多线程会话管理wait notify线程同步修改时间:2026-09-01 20:22:40