1. 从抢锁到协作:理解Java线程状态流转的本质
当我们在Java多线程编程中首次遇到wait()和notify()时,往往会产生这样的困惑:为什么已经有了synchronized这样的锁机制,还需要这套等待通知机制?这就像在一个繁忙的餐厅里,如果所有顾客都挤在柜台前争抢点餐(同步阻塞),反而会降低整体效率。而wait/notify机制相当于让部分顾客先坐下等待(主动释放锁),等餐准备好时再由服务员通知(notify),这种协作模式才是高并发的正确打开方式。
Java中的每个对象都内置了一个等待队列(WaitSet)和一个锁池(EntryList)。当线程调用wait()时,会经历三个关键步骤:首先释放持有的对象锁,然后进入WAITING状态并加入等待队列,最后通过park()挂起线程。这个过程中锁的释放是理解整套机制的核心——它使得其他线程有机会获取锁并改变对象状态,这正是协作式并发的精髓所在。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. wait/notify机制深度解析
2.1 对象监视器模型的三元结构
每个Java对象都关联着一个监视器(Monitor),这个监视器包含三个关键部分:
- 锁计数器(Owner):记录当前持有锁的线程
- 入口队列(EntryList):存放等待获取锁的线程
- 等待队列(WaitSet):存放调用了wait()的线程
当线程A执行以下代码时:
java复制synchronized(obj) {
while(!condition) {
obj.wait();
}
// 处理业务逻辑
}
会发生一系列精妙的状态变化:
- 线程A检查condition不满足,调用obj.wait()
- JVM将A放入obj的WaitSet,释放obj锁
- 线程A进入WAITING状态,CPU调度其他线程
2.2 notify的唤醒策略与陷阱
notify()的唤醒过程同样值得深入理解:
java复制synchronized(obj) {
// 改变condition条件
obj.notify(); // 或notifyAll()
}
此时JVM会:
- 从WaitSet随机选择一个线程(notifyAll()则唤醒所有)
- 将选中线程移入EntryList,状态变为BLOCKED
- 当该线程重新获得锁后,从wait()调用处恢复执行
这里有个关键细节:被唤醒的线程需要重新获取锁才能继续执行,这解释了为什么我们总是要在循环中检查条件:
java复制while(!condition) {
wait();
}
// 而不是
if(!condition) {
wait();
}
因为可能存在虚假唤醒(spurious wakeup),或者多个线程被唤醒但条件又被其他线程改变的情况。
3. join()方法的实现原理与wait/notify的关系
3.1 join的本质是等待线程终止
当我们调用thread.join()时,实际上是在当前线程中等待目标线程终止。查看JDK源码可以发现:
java复制public final synchronized void join(long millis) {
// ...
while (isAlive()) {
wait(millis);
}
// ...
}
这揭示了join()就是基于wait/notify实现的。当线程终止时,JVM会调用notifyAll()通知所有等待该线程的join调用者。
3.2 join与wait/notify的异同对比
| 特性 | wait/notify | join |
|---|---|---|
| 调用对象 | 任意Java对象 | Thread对象 |
| 触发条件 | 需要显式调用 | 线程终止自动触发 |
| 锁释放 | 必须持有锁才能调用 | 不需要显式同步 |
| 使用场景 | 线程间条件协作 | 线程生命周期管理 |
4. 实战中的典型应用场景
4.1 生产者-消费者模型的三种实现
我们来看一个容量限制为1的简化生产者消费者模型:
版本1:基本wait/notify实现
java复制class Buffer {
private Object data;
private boolean empty = true;
public synchronized void put(Object data) {
while (!empty) {
wait();
}
this.data = data;
empty = false;
notifyAll();
}
public synchronized Object take() {
while (empty) {
wait();
}
empty = true;
notifyAll();
return data;
}
}
版本2:使用Lock和Condition
java复制class Buffer {
private final Lock lock = new ReentrantLock();
private final Condition notEmpty = lock.newCondition();
private final Condition notFull = lock.newCondition();
// ... 类似实现,但可以精确通知
}
版本3:BlockingQueue实现
java复制BlockingQueue<Object> buffer = new ArrayBlockingQueue<>(1);
// put()和take()自动处理阻塞
4.2 线程池中的工作线程管理
线程池中的工作者线程通常采用如下模式:
java复制class Worker implements Runnable {
public void run() {
while (!isShutdown) {
Runnable task = null;
synchronized(queue) {
while (queue.isEmpty() && !isShutdown) {
queue.wait();
}
if (!queue.isEmpty()) {
task = queue.poll();
}
}
if (task != null) {
task.run();
}
}
}
public void shutdown() {
synchronized(queue) {
isShutdown = true;
queue.notifyAll();
}
}
}
这种模式结合了wait/notify和中断机制,实现了优雅的线程池关闭。
5. 高频面试问题深度剖析
5.1 wait()为什么必须在同步块中调用?
这是Java设计上的安全考虑。假设允许在非同步代码中调用wait():
- 线程检查condition为false
- 在调用wait()前,其他线程修改condition为true并调用notify()
- 此时原线程再调用wait(),将永远错过通知
这种竞态条件被称为"lost wakeup problem"。同步块保证了检查condition和调用wait()的原子性。
5.2 notify()和notifyAll()如何选择?
考虑一个有多个生产者单个消费者的场景:
- 如果使用notify(),可能唤醒的是另一个生产者,而真正的消费者继续饥饿
- notifyAll()则保证所有等待线程都有机会竞争
经验法则:
- 当所有等待线程是可互换的(处理相同条件),使用notify()
- 当不同线程等待不同条件,使用notifyAll()
5.3 wait()和sleep()的根本区别
| 对比维度 | wait() | sleep() |
|---|---|---|
| 锁行为 | 释放锁 | 不释放任何锁 |
| 调用要求 | 必须在同步上下文中 | 任何地方 |
| 唤醒方式 | 只能被notify/notifyAll唤醒 | 超时或interrupt() |
| 所属类 | Object方法 | Thread静态方法 |
| 使用场景 | 线程间协作 | 单纯的时间等待 |
6. 性能优化与最佳实践
6.1 减少锁竞争的策略
- 缩小同步范围:只同步真正需要互斥的代码块
java复制// 反例
synchronized(this) {
// 耗时操作
result = compute();
wait();
}
// 正例
result = compute(); // 无锁计算
synchronized(this) {
wait();
}
- 使用条件变量替代全局notifyAll
java复制private final Condition dataAvailable = lock.newCondition();
private final Condition spaceAvailable = lock.newCondition();
// 生产者只唤醒消费者,反之亦然
dataAvailable.signal();
6.2 避免死锁的四种模式
- 等待超时机制
java复制synchronized(obj) {
long remaining = TIMEOUT;
long end = System.currentTimeMillis() + remaining;
while (!condition && remaining > 0) {
obj.wait(remaining);
remaining = end - System.currentTimeMillis();
}
}
-
锁排序法:所有线程按固定顺序获取锁
-
开放调用:在调用外部方法时不持有锁
-
使用并发容器:如ConcurrentHashMap替代同步的HashMap
7. 从HotSpot源码看实现细节
深入JVM层面,wait()的实现最终会调用ObjectMonitor::wait():
- 将线程封装成ObjectWaiter节点加入WaitSet
- 调用exit()释放锁
- 通过park()挂起线程
notify()的实现则会:
- 从WaitSet移出ObjectWaiter
- 调用enter()尝试重新获取锁
- 通过unpark()唤醒线程
这些操作都依赖于操作系统的线程调度原语,这也是为什么wait/notify是重量级操作的原因。在超高并发场景下,可以考虑使用AQS或VarHandle等更轻量级的替代方案。
