
本文深入剖析多线程环境下线程尝试从共享 ArrayList 中移除自身导致死锁或数据不一致的根本原因,指出并发修改未加锁是核心问题,并提供基于显式锁保护的正确实践。
本文深入剖析多线程环境下线程尝试从共享 `arraylist` 中移除自身导致死锁或数据不一致的根本原因,指出并发修改未加锁是核心问题,并提供基于显式锁保护的正确实践。
在 Java 并发编程中,一个看似直观的操作——“当前线程从某个共享列表中移除自己”——往往隐藏着严重的线程安全陷阱。您提供的代码中,Passenger.disembark() 方法调用 boatKeeper.getBoat().removePassenger(this) 试图将当前 Passenger 实例从 boat.passengersOnBoard(一个非线程安全的 ArrayList)中移除,但该操作未受任何同步保护,正是引发“幽灵乘客”(ghost passenger)、null 元素残留及 BoatKeeper 无限等待的根本原因。
? 问题根源:非线程安全集合的竞态条件
ArrayList 是典型的非线程安全集合。其 remove(Object o) 方法内部依赖于 indexOf() 和 remove(int index),二者均非原子操作,且在遍历过程中直接修改底层数组结构。当多个线程(如多个 Passenger)并发调用 removePassenger(),或一个线程在 remove() 执行中途被另一个线程(如 BoatKeeper 的 isBoardingComplete() 或 hasFerryingToDo() 中的 boat.hasPassengers())读取 passengersOnBoard 状态时,极易发生以下情况:
-
迭代器失效:若某线程正在遍历列表(如
BoatKeeper检查hasPassengers()),另一线程同时remove(),可能导致ConcurrentModificationException(虽未显式抛出,但内部状态已不一致); -
元素覆盖/跳过:
ArrayList的remove()会将后续元素前移。若两个线程几乎同时移除不同元素,可能因索引错位导致一个元素未被真正清除,或被错误地覆盖为null; -
可见性问题:即使移除成功,由于缺乏内存屏障(如
synchronized或Lock的 happens-before 关系),BoatKeeper线程可能永远看不到passengersOnBoard的最新大小,从而卡在while (boat.hasPassengers()) { condition.await(); }循环中。
您观察到“调试时正常、运行时冻结”的现象,正是因为调试器引入的额外延迟和线程调度变化,偶然掩盖了竞态窗口——这恰恰是典型并发 Bug 的特征。
✅ 正确解法:用统一锁保护所有共享状态访问
修复的关键在于:对 passengersOnBoard 的所有读写操作(包括 addPassenger、removePassenger、hasPassengers、isFull)必须由同一把可重入锁(ReentrantLock)保护,确保互斥与内存可见性。
立即学习“Java免费学习笔记(深入)”;
你接手一个需求时,是不是经常这样:"改一个小字段 → 结果把不相关的代码也改了"、"需求说得很模糊 → 做着做着发现跑偏了"、"数据库加个字段 → 上线才发现漏了数据兼容"、"做了个新功能 → 一个月后自己都忘了当时怎么设计的"、"写到一半被中断 → 再回来不知道从哪继续";这个 Skill 就是专门解决这些问题的。 它会自动探测你的项目环境(JDK、Spring Boot、ORM、数据库、前端框架),记在
原 disembark() 方法的问题代码:
// ❌ 危险:removePassenger() 在锁外执行,无同步保障 boatKeeper.getBoat().removePassenger(this); lock.lock(); condition.signalAll(); lock.unlock();
修正后(需同步整个操作链):
// ✅ 正确:在锁内完成移除 + 通知,保证原子性与可见性
lock.lock();
try {
boatKeeper.getBoat().removePassenger(this);
condition.signalAll(); // 通知 BoatKeeper 等待线程检查状态
} finally {
lock.unlock();
}同时,必须同步 Boat 类中所有访问 passengersOnBoard 的方法。例如,hasPassengers() 和 isFull() 应改为:
boolean hasPassengers() {
lock.lock();
try {
return !passengersOnBoard.isEmpty();
} finally {
lock.unlock();
}
}
boolean isFull() {
lock.lock();
try {
return passengersOnBoard.size() == capacity;
} finally {
lock.unlock();
}
}⚠️ 注意:
Boat类当前未持有lock字段。您需要将其注入(如通过构造函数传入SharedSynchronizers.FAIR_LOCK),或让Boat直接使用SharedSynchronizers.FAIR_LOCK(推荐,保持锁统一)。
?️ 进阶建议:更健壮的设计模式
-
避免“线程自删”反模式:理想情况下,应由协调者(如
BoatKeeper)统一管理乘客生命周期。Passenger可通过回调或事件通知BoatKeeper“我已下船”,由后者在锁内执行移除。 -
考虑线程安全集合:若场景允许,可用
CopyOnWriteArrayList替代ArrayList(适用于读多写少)。但注意其高内存开销与弱一致性(remove()后其他线程可能短暂看到旧快照)。 -
使用
synchronized替代显式锁:若锁粒度合适,synchronized(SharedSynchronizers.FAIR_LOCK)更简洁,但ReentrantLock提供了Condition等高级功能,当前设计中已合理利用。
✅ 总结
| 问题现象 | 根本原因 | 解决方案 |
|---|---|---|
“幽灵乘客”、null 元素、无限等待 |
ArrayList 并发修改未同步 |
所有 passengersOnBoard 访问必须由同一把 Lock 保护 |
| 调试时行为不一致 | 竞态条件受调度时机影响 | 消除竞态,而非依赖调试器延迟 |
修复的核心不是修改单行代码,而是建立共享状态访问的同步契约:只要数据被多个线程读写,就必须通过统一的同步机制(锁、volatile、原子类等)来保证原子性、可见性与有序性。这是 Java 并发编程不可逾越的基石。

















