
本文详解 Tunnel 类中 notify() 无法唤醒等待线程的根本原因,指出同步逻辑缺陷、状态管理错误及原子变量误用问题,并提供线程安全、语义清晰的重构方案。
本文详解 `tunnel` 类中 `notify()` 无法唤醒等待线程的根本原因,指出同步逻辑缺陷、状态管理错误及原子变量误用问题,并提供线程安全、语义清晰的重构方案。
在多线程资源协调场景(如隧道限流)中,wait() 和 notify() 是经典但极易出错的协作机制。原始 Tunnel 实现看似合理,却存在多个关键缺陷,导致等待线程“永远沉睡”或并发越界——这并非 notify() 失效,而是同步契约被破坏所致。
? 核心问题剖析
-
notify() 调用时机与锁持有不匹配
unlock() 中虽调用了 notify(),但其唤醒逻辑依赖于 isOpen == true 的前提。然而,lock() 方法中:- 若 limit 不设置 isOpen = true;
- 仅当 limit >= 3 时才设 isOpen = true,但此时 wait() 已在 while (isOpen) 循环中阻塞;
- 更严重的是:被 notify() 唤醒的线程,在重新获得锁后,未重置 isOpen 或检查 limit,直接跳过条件判断,导致 limit 未递增即进入隧道 —— 这正是答案中指出的“4车并发”风险。
状态语义混乱:isOpen 名不副实
isOpen = false 表示“可通行”,isOpen = true 表示“需等待”,严重违背直觉,极易引发维护性错误。AtomicInteger 的无效使用
所有对 limit 的访问均被 synchronized (this) 包裹,AtomicInteger 的无锁原子性毫无意义;反而 limit.getAndSet(limit.get()-1) 引入竞态(非原子读-改-写),应替换为线程安全的 limit.decrementAndGet() 或直接使用 int。
✅ 正确实现:基于 ReentrantLock + Condition(推荐)
为提升可读性与健壮性,建议使用 java.util.concurrent.locks:
立即学习“Java免费学习笔记(深入)”;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class Tunnel {
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition(); // 隧道未满条件
private int carsInTunnel = 0;
private static final int MAX_CARS = 3;
public void goIntoTunnel() throws InterruptedException {
lock.lock();
try {
// 等待隧道有空位
while (carsInTunnel >= MAX_CARS) {
notFull.await();
}
carsInTunnel++;
System.out.println(Thread.currentThread().getName() + " go into the tunnel");
// 模拟穿越耗时
int timeIntoTunnel = (int) (Math.random() * 5000);
Thread.sleep(timeIntoTunnel);
} finally {
// 退出隧道:释放计数并通知等待者
carsInTunnel--;
notFull.signal(); // 唤醒一个等待线程(公平性可选 signalAll)
System.out.println(Thread.currentThread().getName() + " left the tunnel, time: " + timeIntoTunnel);
lock.unlock(); // 注意:unlock 在 finally 中确保执行
}
}
}⚠️ 注意:lock.unlock() 必须在 finally 块中调用,避免因异常导致死锁。
? 若坚持使用 synchronized + wait()/notify()(基础版)
需严格遵循 “检查-等待-修改-通知” 四步模式,并修复状态管理:
public class Tunnel {
private int carsInTunnel = 0;
private static final int MAX_CARS = 3;
public void goIntoTunnel() throws InterruptedException {
synchronized (this) {
// 【检查 & 等待】
while (carsInTunnel >= MAX_CARS) {
this.wait(); // 等待隧道有空位
}
// 【修改】
carsInTunnel++;
System.out.println(Thread.currentThread().getName() + " go into the tunnel");
}
// 模拟穿越(不在同步块内,避免阻塞其他线程)
int timeIntoTunnel = (int) (Math.random() * 5000);
Thread.sleep(timeIntoTunnel);
// 【退出:修改 + 通知】
synchronized (this) {
carsInTunnel--;
this.notify(); // 唤醒一个等待者
System.out.println(Thread.currentThread().getName() + " left the tunnel, time: " + timeIntoTunnel);
}
}
}✅ 关键总结
| 问题类型 | 错误表现 | 正确做法 |
|---|---|---|
| 条件检查 | while 误用为 if,或检查逻辑缺失 | 必用 while (condition) 防止虚假唤醒 |
| 状态更新 | limit 在唤醒路径中未递增 | 所有状态变更必须在持有同一把锁的前提下完成 |
| 命名与语义 | isOpen = false 表示“开放” → 逻辑反转 | 使用 isFull 或 availableSlots > 0 等自解释布尔 |
| 同步工具选择 | AtomicInteger 在 synchronized 内冗余 | 优先用 int + synchronized,或直接升级 Lock/Condition |
通过以上重构,不仅能彻底解决 notify() 不生效的表象问题,更能构建出可验证、易维护、符合并发编程范式的线程安全组件。记住:wait()/notify() 不是魔法,而是需要精确控制的协作协议——每一次 wait() 都必须对应一次由同一把锁保护的、逻辑正确的 notify()。










