深入理解Java wait()方法:从监视器锁到线程协作的实战解析
1. 从一次线上告警说起:为什么wait()不只是“等待”
那天凌晨,我被一阵急促的告警电话吵醒。监控显示,我们核心交易系统的一个订单处理线程池,CPU使用率异常飙升到90%以上,而队列里却堆积了上万个待处理任务。登录服务器一看,日志里满是java.lang.IllegalMonitorStateException的报错,而报错点,正指向一个我们使用了多年的、看似简单的Object.wait()调用。
这个场景,相信很多Java后端开发都似曾相识。wait()方法,几乎是每个Java工程师入门多线程时必学的“三板斧”之一(另外两个是notify()和notifyAll())。教科书和大多数博客会告诉你:wait()让当前线程释放锁并进入等待状态,直到其他线程调用此对象的notify()或notifyAll()方法。听起来很简单,对吧?但正是这种“简单”的认知,让它在实际生产环境中埋下了无数深坑。
我后来花了整整两个小时才定位到问题:在一个复杂的业务方法中,开发同学为了等待某个外部条件成立,直接调用了wait(5000),但他没有意识到,调用wait()的代码块虽然被synchronized修饰,但锁的对象却不是调用wait()的那个对象。线程在试图释放一个它并未持有的锁时,抛出了异常。更糟糕的是,异常被捕获后仅仅打了行日志,线程继续循环执行,疯狂地尝试获取锁、失败、再尝试,导致了CPU的恶性空转。
这次事故让我彻底反思:我们对wait()的理解,是否还停留在“让线程睡觉”的层面?它背后的监视器锁(Monitor Lock)机制、线程状态切换的精确时机、与notify()的协作陷阱,以及在现代高并发架构下的适用性与替代方案,远比我们想象的要复杂和深刻。这篇文章,我就结合这次踩坑经历和多年的一线实战,带你真正“深入理解Java中的wait()方法”,不仅要知道怎么用,更要明白为什么这么用,以及什么时候不该用。
2. 剥开wait()的洋葱:对象头、监视器与等待集
要理解wait(),绝不能孤立地看它。你必须把它放在Java对象监视器(Monitor)这个整体机制下来审视。很多人以为synchronized关键字就是锁的全部,其实它只是Java内置监视器锁的一个语法糖。而wait(),notify(),notifyAll()正是这套监视器机制中,用于线程间通信的核心原语。
2.1 对象头里的秘密:谁是锁的持有者?
每一个Java对象(除了基本类型)都可以作为一个“监视器锁”。这个能力来源于对象内存布局中的对象头(Object Header)。在HotSpot虚拟机中,对象头主要包含两部分:Mark Word和类型指针。
Mark Word是理解锁状态的关键。在32位JVM中,它占32位;64位JVM中,占64位。为了存储更多信息,它的结构是动态的,会根据对象的状态(无锁、偏向锁、轻量级锁、重量级锁、GC标记)而变化。当我们使用synchronized对一个对象加锁时,JVM最终会操作这个对象的Mark Word,将其指向一个称为Monitor(管程)的数据结构。
这个Monitor(在HotSpot中由ObjectMonitorC++类实现)才是锁的真正实体。它内部维护了几个关键队列:
- _EntryList(入口队列):所有试图进入
synchronized代码块但锁已被占用的线程,会在这里排队等待。 - _WaitSet(等待集合):这就是
wait()方法的核心。当线程调用obj.wait()时,当前线程会被放入这个对象监视器对应的_WaitSet中。 - _owner:指向当前持有该监视器锁的线程。
所以,当你写下synchronized(obj) { obj.wait(); }时,背后发生了一系列精密操作:
- 线程成功进入
synchronized块,意味着它已经成为了obj对应Monitor的_owner。 - 执行
obj.wait(),JVM会检查当前线程是否是obj的Monitor的_owner。如果不是,立刻抛出IllegalMonitorStateException。这就是我线上事故的根源。 - 如果检查通过,线程会释放其对
obj的Monitor的所有权(即_owner置为null),然后将自己(一个ObjectWaiter节点)加入到obj的Monitor的_WaitSet队列中。 - 线程状态由
RUNNABLE变为WAITING(如果调用的是wait(long timeout)则变为TIMED_WAITING),并让出CPU,进入阻塞状态。
关键理解:
wait()释放的锁,是当前线程持有的、与调用wait()的对象相关联的那个监视器锁。如果锁了A对象,却对B对象调用wait(),必然抛异常。这也是为什么wait()必须写在synchronized块内部,因为只有在块内,你才明确持有了某个对象的锁。
2.2 等待与唤醒:一次精准的“交接棒”
线程进入_WaitSet后,就安静地休眠了。那么它如何被唤醒呢?这就要靠notify()或notifyAll()。
notify():JVM会从_WaitSet中随机挑选一个线程(注意,这个“随机”并不是严格意义上的随机,取决于JVM实现,可能是FIFO,也可能是LIFO,所以不能依赖顺序),将其从_WaitSet移出。但移出并不意味着该线程立刻恢复执行。它会被放入_EntryList,参与下一轮对监视器锁的竞争。只有再次成功竞争到锁(成为_owner),它才会从当初调用wait()的地方继续执行。notifyAll():JVM会将_WaitSet中所有线程都移出,并全部放入_EntryList。然后这些线程会同其他在_EntryList中等待的线程一起,公平竞争这把锁。
这里有一个极其重要的细节,也是面试常考点和实战大坑:被notify()唤醒的线程,在从wait()方法返回前,必须重新获得锁!这意味着,即使你是唯一被唤醒的线程,也可能需要等待当前持有锁的线程(比如正在执行notify()的那个线程)退出synchronized块释放锁后,你才能抢到锁并继续运行。
我们用一段代码来演示这个完整的生命周期:
public class WaitNotifyDemo { private static final Object lock = new Object(); private static boolean condition = false; static class WaitingThread extends Thread { @Override public void run() { synchronized (lock) { // 1. 获取锁 System.out.println("等待线程:获取锁,检查条件"); while (!condition) { // 2. 必须用while循环检查条件(重要!) try { System.out.println("等待线程:条件不满足,调用wait()释放锁并等待"); lock.wait(); // 3. 释放锁,线程进入_WaitSet System.out.println("等待线程:被唤醒,但尚未重新获得锁"); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } // 6. 重新获得锁后,从这里继续执行 System.out.println("等待线程:条件满足,执行任务"); } } } static class NotifyingThread extends Thread { @Override public void run() { try { Thread.sleep(1000); // 模拟准备工作耗时 } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock) { // 4. 获取锁 System.out.println("通知线程:获取锁,更改条件"); condition = true; lock.notify(); // 5. 从_WaitSet中唤醒一个线程,将其移入_EntryList System.out.println("通知线程:已发出通知,但尚未释放锁"); try { Thread.sleep(2000); // 模拟通知后还需要做一些工作 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("通知线程:释放锁"); } // 7. 通知线程释放锁,等待线程和其他线程竞争锁 } } public static void main(String[] args) throws InterruptedException { new WaitingThread().start(); new NotifyingThread().start(); } }运行这段代码,你会清晰地看到输出顺序,印证了上述过程。特别注意第2步的while (!condition),这是使用wait()的黄金法则,我将在下一章详细解释为什么不能用if。
3. 从“能用”到“用好”:wait/notify的正确姿势与经典陷阱
理解了底层机制,我们来看看如何正确使用它,以及那些教科书里不会写的“坑”。
3.1 黄金法则:永远在循环中检查条件
这是使用wait()最最重要的一条规则。绝大多数初级错误都源于违反了它。看一个错误示例:
// 错误示范! synchronized (lock) { if (!condition) { // 使用 if 判断 lock.wait(); } // 假设条件已满足,执行任务... }这个代码的问题在于虚假唤醒(Spurious Wakeup)。线程是可能在没有收到任何notify()或notifyAll()调用,也没有超时或被中断的情况下,从wait()状态返回的。这种现象在POSIX线程库和Java规范中都是被允许的。虽然不常发生,但一旦发生,如果使用if,线程就会错误地认为条件已满足,继续执行后续逻辑,可能导致数据不一致、状态异常等严重问题。
正确的做法是使用while循环:
// 正确示范 synchronized (lock) { while (!condition) { // 使用 while 循环 lock.wait(); } // 只有条件真正满足时,才会执行到这里 // 执行任务... }while循环保证了即使发生虚假唤醒,线程也会再次检查条件。如果条件仍未满足,它会再次调用wait()进入等待。这是保护性暂停(Guarded Suspension)模式的典型实现。
3.2 丢失的信号与过早的唤醒
与虚假唤醒相对的另一个问题是信号丢失(Missed Signal)。考虑以下时序:
- 线程A检查条件,发现不满足,但在它调用
wait()之前,发生了线程调度。 - 线程B获得了锁,修改了条件使其为真,并调用了
notify()。由于此时_WaitSet为空,这个通知信号被丢弃了。 - 线程A恢复执行,调用
wait()并进入等待。此时条件已经满足,但通知信号已经丢失,线程A可能永远等下去。
这就是为什么,修改条件的代码和调用wait()的代码,必须在同一个锁的保护下,并且条件的检查与等待必须是原子的。我们上面的正确示范代码就保证了这一点:检查条件、决定等待,这两个操作在synchronized块内是连续的、原子的,不会被其他修改条件的线程打断。
3.3 notify() 与 notifyAll() 的抉择
这是一个经典的面试题。简单来说:
notify():唤醒一个等待线程。优点是效率高,减少不必要的线程竞争和上下文切换。缺点是选择是随机的,可能导致某些线程“饥饿”(长时间不被唤醒),特别是当等待线程有不同的等待条件时。notifyAll():唤醒所有等待线程。优点是公平,所有等待线程都有机会被唤醒并重新检查条件。缺点是性能开销大,会引发“惊群效应”,大量线程被唤醒去竞争锁,但最终可能只有一个(或少数)能继续执行,其他线程白白经历了唤醒-竞争-阻塞的过程。
如何选择?
- 当所有等待线程都在等待同一个条件,并且该条件被满足时,任意一个线程被唤醒都能处理时,使用
notify()是高效的选择。典型例子是线程池的任务队列,一个任务被放入,唤醒一个工作线程即可。 - 当存在多个不同的等待条件,或者被唤醒的线程可能发现条件仍不满足而需要再次等待时,必须使用
notifyAll()。例如,一个有限大小的缓冲区,可能有生产者在等待“非满”条件,消费者在等待“非空”条件。如果只唤醒一个,可能唤醒的是生产者,但缓冲区已满,它还得继续等,而真正该被唤醒的消费者却没人叫。使用notifyAll()能确保所有相关方都来检查一下自己的条件。
在实践中,如果你无法确定,或者代码结构比较复杂,倾向于使用notifyAll()更为安全。在当今多核处理器环境下,由错误使用notify()导致的bug,其调试成本远高于notifyAll()带来的微小性能开销。
3.4 中断处理:优雅地退出等待
wait()方法会抛出InterruptedException。这意味着等待中的线程可以被其他线程调用其interrupt()方法中断。这是一个重要的线程协作和取消机制。
处理中断的推荐做法是:
synchronized (lock) { while (!condition) { try { lock.wait(); } catch (InterruptedException e) { // 1. 清理当前任务状态(如果需要) // 2. 通常选择重新设置中断状态,让上层调用者感知 Thread.currentThread().interrupt(); // 3. 根据业务逻辑,可以选择退出循环或继续等待 // 例如,如果中断意味着任务取消,则退出 break; } } if (!Thread.currentThread().isInterrupted()) { // 执行正常任务 } }捕获InterruptedException后,通常应该调用Thread.currentThread().interrupt()来重新设置中断标志。因为捕获异常会清除中断状态。这样,方法的调用者可以通过isInterrupted()来检查是否发生了中断,从而做出相应的处理(比如回滚事务、清理资源、终止线程等)。忽略中断(空catch块)是非常糟糕的做法。
4. 超越内置锁:wait/notify在现代并发编程中的定位
synchronized配合wait()/notify()是Java最原生的线程间通信机制。但随着Java并发包(java.util.concurrent, 简称JUC)的引入,我们有了更多、更强大、更安全的选择。那么,在什么情况下我们还应使用wait()/notify(),又该何时转向JUC呢?
4.1 对比分析:synchronized+wait/notify vs JUC工具
| 特性 | synchronized+wait()/notify() | java.util.concurrent工具类 |
|---|---|---|
| 抽象层次 | 低层、基于监视器原语 | 高层、提供了丰富的并发抽象(锁、队列、屏障等) |
| 灵活性 | 较低。锁的获取和释放必须严格配对(synchronized块),且是独占锁。 | 极高。ReentrantLock可尝试获取、可定时、可中断、可设置公平性;Condition支持多个等待队列。 |
| 功能性 | 基础等待/通知。一个对象只有一个等待队列。 | 强大。CountDownLatch(闭锁)、CyclicBarrier(栅栏)、Semaphore(信号量)、BlockingQueue(阻塞队列)等,针对不同场景。 |
| 易用性与安全性 | 容易出错(如忘记synchronized、虚假唤醒、信号丢失)。 | 更安全,API设计更友好,减少了犯错的可能。 |
| 性能 | 早期版本性能较差,但经过持续优化(偏向锁、轻量级锁),在低竞争场景下已非常好。 | ReentrantLock在超高竞争场景下可能表现更好,但通常差异不大。JUC类的设计更利于构建复杂并发程序。 |
4.2 Condition接口:更灵活的等待/通知
如果你需要wait()/notify()的功能,但又需要更灵活的控制,ReentrantLock搭配Condition接口是绝佳的升级选择。
一个ReentrantLock可以创建多个Condition对象(通过lock.newCondition())。每个Condition就相当于一个独立的等待队列。这完美解决了notify()唤醒线程不确定性的问题。
例如,实现一个简单的有界阻塞队列:
public class BoundedBlockingQueue<T> { private final ReentrantLock lock = new ReentrantLock(); private final Condition notFull = lock.newCondition(); // 队列“未满”条件 private final Condition notEmpty = lock.newCondition(); // 队列“非空”条件 private final Object[] items; private int putPtr, takePtr, count; public BoundedBlockingQueue(int capacity) { items = new Object[capacity]; } public void put(T x) throws InterruptedException { lock.lock(); try { while (count == items.length) { // 队列满,等待“未满”条件 notFull.await(); // 相当于 wait(),但挂在notFull这个条件队列上 } items[putPtr] = x; if (++putPtr == items.length) putPtr = 0; ++count; notEmpty.signal(); // 相当于 notify(),但只唤醒在notEmpty上等待的线程 } finally { lock.unlock(); } } public T take() throws InterruptedException { lock.lock(); try { while (count == 0) { // 队列空,等待“非空”条件 notEmpty.await(); } @SuppressWarnings("unchecked") T x = (T) items[takePtr]; items[takePtr] = null; if (++takePtr == items.length) takePtr = 0; --count; notFull.signal(); // 只唤醒在notFull上等待的线程 return x; } finally { lock.unlock(); } } }可以看到,Condition.await()和Condition.signal()完全对应Object.wait()和Object.notify(),但语义更清晰,并且解决了“不同条件共享一个等待队列”的问题。生产者只唤醒消费者,消费者只唤醒生产者,效率更高,逻辑更清晰。
4.3 实战建议:何时选择wait/notify?
尽管JUC功能强大,但wait()/notify()依然有其存在价值:
- 维护遗留代码:很多老系统仍在使用,你需要理解它。
- 极简场景:如果你只是需要一个非常简单的、单条件的线程间等待,并且代码结构极其简单清晰,使用
synchronized+wait()/notify()反而更简洁直观。 - 对性能有极致要求且场景匹配:在你知道所有线程都在等待同一个条件,并且
notify()的随机唤醒完全符合需求时,它可能比使用Condition或更重的JUC组件有微小的性能优势(通常可忽略不计)。
对于新项目或新代码,我的个人建议是:
- 优先考虑
BlockingQueue、CountDownLatch、Semaphore等高级抽象。它们直接解决了生产者-消费者、线程计数、资源池等常见模式,几乎不需要你手动处理锁和条件。 - 如果需要复杂的条件等待,使用
ReentrantLock和Condition。它的灵活性远超内置锁。 - 将
synchronized+wait()/notify()作为最后的选择,或者在你非常确信其简单性和适用性时使用。使用时务必严格遵守“循环检查条件”和“在持有正确锁的对象上调用”的规则。
5. 性能调优与监控:当wait成为瓶颈时
在复杂的生产环境中,线程长时间处于WAITING或TIMED_WAITING状态是正常的。但如果大量线程异常地卡在wait()上,可能就是性能瓶颈或死锁的前兆。我们需要一套方法来监控和诊断。
5.1 线程转储分析:识别等待的根源
最直接的诊断工具是线程转储(Thread Dump)。通过jstack <pid>命令或发送SIGQUIT信号给JVM进程可以获取。
一个典型的在wait()上等待的线程堆栈如下:
"Consumer-Thread-1" #15 prio=5 os_prio=0 tid=0x00007f1234567800 nid=0x5e3 waiting on condition [0x00007f11abcd0000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - waiting on <0x00000000ff456789> (a java.util.ArrayList) at java.lang.Object.wait(Object.java:502) at com.example.MyQueue.take(MyQueue.java:42) - locked <0x00000000ff456789> (a java.util.ArrayList) at com.example.Consumer.run(Consumer.java:25)关键信息解读:
WAITING (on object monitor):线程状态是WAITING,原因是在对象监视器上等待。waiting on <0x...> (a java.util.ArrayList):它正在等待哪个对象(地址0x00000000ff456789,类型ArrayList)。这能帮你快速定位到是哪个锁/条件出了问题。locked <0x...> (a java.util.ArrayList):它当前持有哪个对象的锁。注意,在wait()时,线程已经释放了这个锁。这里显示的是它进入wait()前持有的锁,或者是从wait()唤醒后重新获得的锁(如果堆栈是在唤醒后、执行后续代码前抓取的,情况会比较复杂)。- locked <...>行:这行表示该线程堆栈帧当前持有一个锁。对于WAITING状态的线程,这通常意味着它是在持有该锁的情况下调用了wait()。
通过分析多个线程的堆栈,你可以构建出线程间的依赖和等待关系图。如果发现线程A持有锁L1,等待锁L2;而线程B持有锁L2,等待锁L1,这就是经典的死锁。如果发现大量线程都在等待同一个对象(比如一个任务队列),而没有一个线程去notify(),那可能就是生产者出了问题或条件判断逻辑有误。
5.2 超时机制:为wait加上“安全阀”
无期限的wait()是危险的,它可能因为程序逻辑错误(如信号丢失)导致线程永久挂起。强烈建议使用带有超时参数的wait(long timeout)方法。
synchronized (lock) { long deadline = System.currentTimeMillis() + 5000; // 5秒超时 long remaining = 5000; while (!condition && remaining > 0) { lock.wait(remaining); // 被唤醒或超时后,重新计算剩余时间 remaining = deadline - System.currentTimeMillis(); } if (condition) { // 条件满足,执行任务 } else { // 超时,执行降级或告警逻辑 log.warn("等待条件超时,执行降级策略"); // 例如:返回默认值、抛出特定异常、记录错误指标等 } }引入超时后,线程最坏情况下也只会阻塞指定的时间。超时后,线程可以执行一些降级逻辑、记录告警、或者进行一些资源清理,然后优雅地退出或重试。这大大增强了系统的健壮性。
5.3 监控指标:将等待可视化
在微服务和云原生架构下,我们需要将线程等待情况指标化、可视化。
自定义监控:在调用
wait()的地方,可以使用Micrometer、Dropwizard Metrics等工具,打点记录等待开始和结束的时间,计算等待时长分布。对于超时的情况,记录超时次数。Timer.Sample sample = Timer.start(registry); try { lock.wait(timeout); } finally { sample.stop(timer); // timer记录等待耗时 }JVM内置MXBean:通过
ThreadMXBean可以获取所有线程的状态信息,定期采集Thread.State.WAITING和TIMED_WAITING状态的线程数量,观察其变化趋势。APM工具:像SkyWalking、Pinpoint这类应用性能监控工具,可以自动追踪线程的阻塞时间,并将其关联到具体的代码行和方法,是定位
wait()相关性能问题的利器。
当监控图表显示某个条件的平均等待时间持续增长,或超时比例异常升高时,就是你需要介入调查的信号。可能是下游服务变慢、任务处理能力不足,或者就是出现了本章开头提到的逻辑错误。
6. 庖丁解牛:从HotSpot源码看wait/notify的实现
对于追求极致的开发者,看一眼JVM的底层实现(这里以OpenJDK HotSpot为例)能让我们对wait()的理解更加透彻。虽然我们日常不写C++代码,但了解其原理有助于我们预判其行为。
Object.wait()的本地方法实现在jdk/src/hotspot/share/runtime/objectMonitor.cpp文件中。核心函数是ObjectMonitor::wait()。
其简化后的核心逻辑如下:
- 检查与准备:检查当前线程是否是此ObjectMonitor的
_owner(即锁的持有者),如果不是,抛出IllegalMonitorStateException。然后,将当前线程封装成一个ObjectWaiter节点。 - 加入等待集:将这个
ObjectWaiter节点通过AddWaiter()函数加入到_WaitSet这个双向链表中。_WaitSet维护了所有在此监视器上调用wait()的线程。 - 释放锁:调用
exit()函数释放当前线程持有的锁。这会修改_owner指针,并可能唤醒_EntryList或_cxq(另一个竞争队列)中的下一个线程。 - 挂起线程:调用
park()函数(Linux下通常是pthread_cond_wait)将当前线程挂起,让出CPU。 - 被唤醒后:当其他线程调用
notify()/notifyAll(),或等待超时,或线程被中断时,线程从park()中返回。然后,它需要调用enter()函数尝试重新获取锁。注意,这里需要竞争,可能抢不过其他线程,所以它可能会再次被放入_EntryList排队。 - 清理与返回:成功获取锁后,将
ObjectWaiter节点从_WaitSet中移除,然后从wait()方法返回,继续执行用户代码。
notify()的实现(ObjectMonitor::notify())则相对简单:它从_WaitSet链表中取出一个ObjectWaiter节点(notifyAll()则取出所有),然后根据策略(默认是先将节点放入_cxq队列)将其移动到竞争队列中,等待锁的释放以便参与竞争。
几个关键洞察:
- “虚假唤醒”的根源:
park()系统调用(或pthread_cond_wait)在少数情况下可能在没有外部信号的情况下返回,这是操作系统层面的行为,JVM规范允许,所以Java层面必须用while循环来防御。 - 锁的重入性:
wait()会完全释放锁,这与ReentrantLock的Condition.await()行为一致。这意味着,即使线程重入了synchronized块多次,一次wait()调用也会释放所有重入计数。 - 性能考量:
_WaitSet和_EntryList的管理、线程的挂起与唤醒,都涉及操作系统调用,是相对昂贵的操作。这就是为什么在高性能场景下,我们有时会避免使用阻塞操作,而采用自旋(Spin)或基于CAS的无锁算法。
理解这些底层细节,你就不会再觉得wait()是一个黑盒。当你在日志中看到线程状态、分析死锁时,脑海中能清晰地映射出_WaitSet、_EntryList这些队列的运作画面,解决问题的思路也会更加清晰。
7. 反模式与最佳实践总结
回顾我开篇提到的线上问题,以及我们讨论的种种细节,我们可以总结出一些必须遵守的“军规”和值得推荐的实践。
必须避免的反模式:
- 不在同步块内调用wait/notify:这是致命错误,直接导致
IllegalMonitorStateException。 - 使用if而非while检查条件:对虚假唤醒毫无抵抗力,程序行为不确定。
- 忽略InterruptedException:空catch块会吞噬中断信号,使得线程无法响应取消请求,可能导致资源无法释放或应用无法关闭。
- 在持有锁时执行耗时操作:在
synchronized块内或调用notify()后执行长时间I/O、网络请求或复杂计算,会严重降低系统吞吐量,因为其他线程都在等待这把锁。 - 错误地使用notify():当存在多个等待条件时,使用
notify()可能导致信号发送给错误的等待者,造成某些线程饥饿。
推荐的最佳实践:
- 模板化编码:将
wait()的使用封装成一个固定模板,确保不会遗漏while循环和中断处理。public void awaitCondition() throws InterruptedException { synchronized (lock) { while (!conditionMet()) { lock.wait(); } // 条件满足后的逻辑 doSomething(); } } public void signalCondition() { synchronized (lock) { updateCondition(); lock.notifyAll(); // 或 lock.notify() } } - 总是使用超时:给你的
wait()加上一个合理的超时时间,这是系统韧性的重要保障。 - 明确锁对象:使用一个私有的、final的Object专门作为锁对象(
private final Object lock = new Object();),而不是锁住业务对象或this。这可以避免外部代码意外干扰你的同步逻辑。 - 优先使用JUC:对于新建项目,优先评估
BlockingQueue、CountDownLatch、Semaphore、CyclicBarrier、CompletableFuture等高级并发工具是否能满足需求。它们更安全、更强大。 - 代码审查关注点:在代码审查中,对任何
wait()/notify()的使用都要格外警惕,重点检查锁对象、条件检查循环、中断处理和notify/notifyAll的选择是否正确。
wait()方法就像Java并发世界里的“古老技艺”,它强大而原始。理解它,不仅是为了应对遗留代码和面试提问,更是为了深入理解Java并发模型的基石。当你透彻掌握了wait()背后的监视器、等待集、状态转换,你再看ReentrantLock、Condition乃至各种高级并发工具时,会有一种“一览众山小”的通透感。在复杂的分布式系统里,线程间的协作不过是放大了的进程间通信,其核心思想——互斥、同步、条件等待——是相通的。这次线上故障虽然让我熬了个夜,但也让我把这套基础重新打磨了一遍。现在,当我再看到IllegalMonitorStateException时,我看到的不是一行报错,而是一幅清晰的线程状态流转图。这大概就是所谓“深入理解”带来的底气吧。
