为什么wait()必须在synchronized里?这不是规定,这是必然!让我们揭开Monitor锁的神秘面纱。
一、开场:一个IllegalMonitorStateException💥
代码炸弹
public class WrongWaitNotify {
public static void main(String[] args) {
Object lock = new Object();
// ❌ 没有synchronized
lock.wait(); // 💣 抛出IllegalMonitorStateException!
}
}
异常信息:
Exception in thread "main" java.lang.IllegalMonitorStateException
at java.lang.Object.wait(Native Method)
为什么会报错? 因为没有持有Monitor锁!
二、什么是Monitor?🔍
Monitor对象监视器
每个Java对象都有一个Monitor(监视器),它是一种同步机制,包含:
- Owner(所有者):当前持有锁的线程
- EntryList(入口队列):等待获取锁的线程
- WaitSet(等待集合):调用wait()后等待的线程
可视化:
Monitor
┌─────────────────────────────────┐
│ Owner: Thread-1 │
├─────────────────────────────────┤
│ EntryList (等待锁): │
│ Thread-2 ──→ Thread-3 │
├─────────────────────────────────┤
│ WaitSet (wait): │
│ Thread-4, Thread-5 │
└─────────────────────────────────┘
Monitor的工作流程
1. 线程进入synchronized块
→ 尝试获取Monitor
2. 获取成功
→ 成为Owner
→ 执行代码
3. 获取失败
→ 进入EntryList
→ 阻塞等待
4. Owner释放锁
→ 唤醒EntryList中的一个线程
→ 竞争Monitor
三、为什么wait()必须在synchronized中?🤔
原因1:避免竞态条件(Race Condition)
错误示例(假设没有强制要求):
// 假设wait/notify不需要synchronized
public class BrokenProducerConsumer {
private Queue<String> queue = new LinkedList<>();
private Object lock = new Object();
// 生产者
public void produce(String item) {
queue.add(item);
lock.notify(); // ① 通知消费者
}
// 消费者
public String consume() {
if (queue.isEmpty()) { // ② 检查队列
lock.wait(); // ③ 等待
}
return queue.poll(); // ④ 取出元素
}
}
时序问题:
时刻1: 消费者执行②,发现队列空
时刻2: 生产者执行①,notify()(但消费者还没wait!)
时刻3: 消费者执行③,wait()(永远等待,因为notify已经发过了!)
这就是Lost Wake-Up Problem(丢失唤醒)!
正确做法:
public class CorrectProducerConsumer {
private Queue<String> queue = new LinkedList<>();
public synchronized void produce(String item) {
queue.add(item);
notify(); // 在锁内notify
}
public synchronized String consume() throws InterruptedException {
while (queue.isEmpty()) { // 循环检查
wait(); // 在锁内wait
}
return queue.poll();
}
}
为什么加synchronized就对了?
消费者:
1. 获取锁
2. 检查条件(queue.isEmpty())
3. 调用wait()
→ 原子操作:释放锁 + 进入WaitSet
生产者:
1. 获取锁(此时消费者已经在WaitSet中)
2. 修改状态(queue.add())
3. 调用notify()
4. 释放锁
→ 消费者被唤醒,重新竞争锁
关键: wait()会原子地释放锁并进入等待,避免了时间窗口!
原因2:保证可见性
public class VisibilityProblem {
private boolean ready = false;
private int data = 0;
// 线程1
public void produce() {
data = 42; // ①
ready = true; // ②
// notify(); // ③ 假设不需要synchronized
}
// 线程2
public void consume() {
// while (!ready) {
// wait(); // ④ 假设不需要synchronized
// }
System.out.println(data); // ⑤ 可能看不到data=42!
}
}
没有synchronized:
- 线程1的写入可能不可见
- 可能发生指令重排序
有synchronized:
- happens-before保证可见性
- 内存屏障防止重排序
原因3:保护共享状态
public class StateProtection {
private int count = 0;
private Object lock = new Object();
// 假设wait不需要synchronized
public void decrement() {
if (count == 0) { // ① 读count
lock.wait(); // ② 等待
}
count--; // ③ 修改count
}
public void increment() {
count++; // ④ 修改count
lock.notify(); // ⑤ 通知
}
}
问题:
- ①和③之间,count可能被其他线程修改
- 没有锁保护,count可能变成负数!
解决:
public synchronized void decrement() throws InterruptedException {
while (count == 0) {
wait(); // 释放锁后,其他线程才能修改count
}
count--;
}
四、wait()的完整工作流程🔄
步骤详解
synchronized (lock) {
while (!condition) {
lock.wait(); // ← 这里发生了什么?
}
// 条件满足,继续执行
}
wait()的原子操作:
1. 检查:当前线程是否持有Monitor锁
→ 没有?抛出IllegalMonitorStateException
→ 有?继续
2. 释放:释放Monitor锁
→ Owner置为null
→ 当前线程从Owner移到WaitSet
3. 阻塞:线程进入WAITING状态
→ 等待notify()/notifyAll()
→ 或者被interrupt()
4. 唤醒后:重新竞争锁
→ 从WaitSet移到EntryList
→ 获取到锁后,从wait()返回
→ 继续执行后续代码
状态转换图:
RUNNABLE (持有锁)
|
调用wait()
↓
WAITING (在WaitSet)
|
被notify()
↓
BLOCKED (在EntryList)
|
获取到锁
↓
RUNNABLE (继续执行)
五、notify()的工作流程📢
notify()做了什么?
synchronized (lock) {
// 修改条件
lock.notify(); // ← 唤醒一个等待线程
}
步骤:
1. 检查:当前线程是否持有Monitor锁
→ 没有?抛出IllegalMonitorStateException
2. 唤醒:从WaitSet中选一个线程
→ 移到EntryList
→ 线程状态:WAITING → BLOCKED
3. 注意:notify()不会立即释放锁!
→ 当前线程退出synchronized后才释放
→ 被唤醒的线程才能竞争锁
notify() vs notifyAll()
notify(): 唤醒一个(随机)
synchronized (lock) {
lock.notify(); // 只唤醒一个线程
}
notifyAll(): 唤醒所有
synchronized (lock) {
lock.notifyAll(); // 唤醒所有等待线程
}
什么时候用notifyAll?
public class MultiCondition {
private int data = 0;
// 多个条件
public synchronized void waitForPositive() throws InterruptedException {
while (data <= 0) {
wait();
}
}
public synchronized void waitForNegative() throws InterruptedException {
while (data >= 0) {
wait();
}
}
public synchronized void setData(int value) {
data = value;
notifyAll(); // 必须用notifyAll,因为不知道该唤醒谁
}
}
建议: 除非性能要求极高,否则优先用notifyAll(),避免信号丢失。
六、经典案例:生产者-消费者模式🏭
完整实现
public class ProducerConsumerQueue<T> {
private final Queue<T> queue = new LinkedList<>();
private final int capacity;
public ProducerConsumerQueue(int capacity) {
this.capacity = capacity;
}
// 生产者
public synchronized void put(T item) throws InterruptedException {
// 循环检查(防止虚假唤醒)
while (queue.size() == capacity) {
System.out.println(Thread.currentThread().getName() + " 队列满,等待...");
wait(); // 释放锁,进入WaitSet
}
queue.add(item);
System.out.println(Thread.currentThread().getName() + " 生产:" + item);
notifyAll(); // 唤醒消费者
}
// 消费者
public synchronized T take() throws InterruptedException {
// 循环检查(防止虚假唤醒)
while (queue.isEmpty()) {
System.out.println(Thread.currentThread().getName() + " 队列空,等待...");
wait(); // 释放锁,进入WaitSet
}
T item = queue.poll();
System.out.println(Thread.currentThread().getName() + " 消费:" + item);
notifyAll(); // 唤醒生产者
return item;
}
}
// 测试
public class Test {
public static void main(String[] args) {
ProducerConsumerQueue<Integer> queue = new ProducerConsumerQueue<>(5);
// 生产者
new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
queue.put(i);
Thread.sleep(100);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "生产者").start();
// 消费者
new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
queue.take();
Thread.sleep(200); // 消费慢
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "消费者").start();
}
}
输出:
生产者 生产:0
生产者 生产:1
消费者 消费:0
生产者 生产:2
消费者 消费:1
生产者 生产:3
生产者 生产:4
生产者 生产:5
生产者 队列满,等待...
消费者 消费:2
生产者 生产:6
...
七、为什么必须用while而不是if?⚠️
虚假唤醒(Spurious Wakeup)
错误示例:
// ❌ 用if
public synchronized T take() throws InterruptedException {
if (queue.isEmpty()) {
wait();
}
return queue.poll(); // 可能还是空的!
}
问题场景:
1. 消费者A:检查队列空,wait()
2. 消费者B:检查队列空,wait()
3. 生产者:放入一个元素,notifyAll()
4. 消费者A:被唤醒,取走元素
5. 消费者B:被唤醒,队列又空了!
→ 执行poll(),返回null或抛异常!
正确示例:
// ✅ 用while
public synchronized T take() throws InterruptedException {
while (queue.isEmpty()) { // 循环检查
wait();
}
return queue.poll(); // 确保队列不为空
}
原因:
wait()醒来后,条件可能已经不满足了- 必须重新检查条件
- while保证条件始终满足
八、wait() vs sleep() vs park()⏰
| 特性 | wait() | sleep() | LockSupport.park() |
|---|---|---|---|
| 所属类 | Object | Thread | LockSupport |
| 需要锁 | ✅ 必须 | ❌ 不需要 | ❌ 不需要 |
| 释放锁 | ✅ 释放 | ❌ 不释放 | ❌ 不释放 |
| 唤醒方式 | notify/notifyAll | 时间到/interrupt | unpark/interrupt |
| 用途 | 线程协作 | 延时 | 底层阻塞工具 |
对比示例
// wait() - 线程协作
synchronized (lock) {
while (!condition) {
lock.wait(); // 释放锁,等待notify
}
}
// sleep() - 延时
Thread.sleep(1000); // 不释放锁,单纯睡眠
// park() - 底层工具
LockSupport.park(); // 阻塞当前线程
LockSupport.unpark(thread); // 唤醒指定线程
九、现代替代方案:Condition⚡
ReentrantLock + Condition
public class ModernQueue<T> {
private final Queue<T> queue = new LinkedList<>();
private final int capacity;
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
public ModernQueue(int capacity) {
this.capacity = capacity;
}
public void put(T item) throws InterruptedException {
lock.lock();
try {
while (queue.size() == capacity) {
notFull.await(); // 等价于wait()
}
queue.add(item);
notEmpty.signal(); // 等价于notify()
} finally {
lock.unlock();
}
}
public T take() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await();
}
T item = queue.poll();
notFull.signal();
return item;
} finally {
lock.unlock();
}
}
}
优势:
- ✅ 多个Condition,精确唤醒
- ✅ 可中断、可超时
- ✅ 公平锁选项
十、常见陷阱与最佳实践⚠️
陷阱1:忘记循环检查
// ❌ 错误
if (condition) {
wait();
}
// ✅ 正确
while (!condition) {
wait();
}
陷阱2:在循环外修改共享状态
// ❌ 错误
synchronized (lock) {
while (!ready) {
lock.wait();
}
}
data = processData(); // 没有锁保护!
// ✅ 正确
synchronized (lock) {
while (!ready) {
lock.wait();
}
data = processData(); // 在锁内
}
陷阱3:notify后立即修改状态
// ⚠️ 小心
synchronized (lock) {
lock.notify();
// 这里继续修改状态,被唤醒的线程要等待
doSomeWork();
}
// ✅ 更好
synchronized (lock) {
doSomeWork();
lock.notify(); // 最后notify
}
最佳实践
- 总是用while检查条件
- wait/notify必须在synchronized中
- 优先用notifyAll而不是notify
- 避免在notify后做耗时操作
- 考虑用Condition替代wait/notify
十一、面试高频问答💯
Q1: 为什么wait()是Object的方法而不是Thread的?
A: 因为wait()操作的是Monitor(对象监视器),每个对象都有Monitor,所以定义在Object中。Thread的方法是操作线程本身(如sleep、interrupt)。
Q2: notify()会立即释放锁吗?
A: 不会! notify()只是把线程从WaitSet移到EntryList,当前线程退出synchronized块后才释放锁。
Q3: 为什么wait()会抛出InterruptedException?
A: 因为等待的线程可能被interrupt()中断,需要响应中断请求。
Q4: wait()和sleep()的本质区别是什么?
A:
- wait():释放锁,用于线程协作
- sleep():不释放锁,单纯延时
Q5: 可以不用synchronized,直接调用wait()吗?
A: 不能! 会抛出IllegalMonitorStateException。这是设计上的强制要求,保证正确性。
十二、底层实现:ObjectMonitor源码🔧
HotSpot VM中的ObjectMonitor结构(C++):
ObjectMonitor() {
_header = NULL;
_count = 0; // 重入计数
_waiters = 0; // 等待线程数
_recursions = 0; // 重入次数
_object = NULL;
_owner = NULL; // 持有锁的线程
_WaitSet = NULL; // wait()的线程队列
_EntryList = NULL; // 等待锁的线程队列
_cxq = NULL; // 竞争队列
}
wait()的C++实现(简化):
void ObjectMonitor::wait(jlong millis, TRAPS) {
Thread * const Self = THREAD;
// 1. 保存锁状态
intptr_t save = _recursions;
_recursions = 0;
// 2. 退出锁
exit(true, Self);
// 3. 加入WaitSet
AddWaiter(&node);
// 4. 阻塞等待
Self->_ParkEvent->park();
// 5. 被唤醒后,重新获取锁
enter(Self);
_recursions = save;
}
十三、总结:Monitor锁的铁律📜
核心原则:
-
wait/notify必须在synchronized中
- 保证原子性(避免Lost Wake-Up)
- 保证可见性(happens-before)
- 保护共享状态
-
用while而不是if
- 防止虚假唤醒
- 确保条件始终满足
-
优先用notifyAll
- 避免信号丢失
- 代码更健壮
记忆口诀:
synchronized保护Monitor锁, wait释放锁进WaitSet, notify唤醒竞争EntryList, while循环防止虚假醒。
下期预告: 线程中断机制:如何优雅地停止线程?interrupt()的正确姿势!🛑