为何synchronized、wait/notifyAll必须作用于同一对象?附问题案例
我在尝试编写一个结合wait()和synchronized的线程同步Demo时遇到了问题,代码如下:
public class WaitZero { private static AtomicInteger num = new AtomicInteger(0); private static boolean consumed = false; public static void main(String... args) throws Exception { ThreadPoolExecutor threadPoolExecutor = getMyCachedThreadPool(); for (int i = 0; i < 5; i++) { threadPoolExecutor.submit(WaitZero::send); threadPoolExecutor.submit(WaitZero::receive); } threadPoolExecutor.shutdown(); threadPoolExecutor.awaitTermination(60, TimeUnit.SECONDS); } private static synchronized void send() { try { while (!isConsumed()) { num.wait(); } } catch (InterruptedException ignored) { ignored.printStackTrace(); } num.incrementAndGet(); System.out.println(Thread.currentThread().getName() + " number updated: " + num); setConsumed(false); num.notifyAll(); } private static synchronized void receive() { try { while (isConsumed()) { num.wait(); } } catch (InterruptedException ignored) { ignored.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " number received: " + num); setConsumed(true); num.notifyAll(); // ToDo: when to use notify? // ToDo: what is monitor? } private static boolean isConsumed() { return consumed; } private static void setConsumed(boolean consumed) { WaitZero.consumed = consumed; } }
这个Demo的输出非常不稳定,比如某次输出是:
shared-pool-0 number received: 0
shared-pool-1 number updated: 1
shared-pool-0 number received: 1
shared-pool-1 number updated: 2
shared-pool-1 number received: 2
shared-pool-2 number updated: 3
但我预期的是send和receive线程严格交替执行,输出应该是这样的:
shared-pool-1 number received: 0
shared-pool-0 number updated: 1
shared-pool-3 number received: 1
shared-pool-2 number updated: 2
shared-pool-1 number received: 2
shared-pool-0 number updated: 3
shared-pool-2 number received: 3
shared-pool-3 number updated: 4
shared-pool-5 number received: 4
shared-pool-4 number updated: 5
后来我把wait()/notifyAll()的调用对象从num改成WaitZero.class后,就得到了正确的结果。我知道synchronized、wait()/notifyAll()必须作用于同一对象才能保证正确性,但想具体了解如果不这么做,会引发什么具体的问题?
核心问题:锁对象与等待监视器不匹配,违反了wait/notify的规则
先给你拆解清楚底层的逻辑,你就能明白为什么会出现这种混乱:
1. 先明确两个关键规则
- synchronized静态方法的锁对象:当你用
synchronized修饰静态方法时,线程进入这个方法时持有的锁是当前类的Class对象(也就是WaitZero.class),而不是你定义的num变量。 - wait/notify的前置条件:调用
obj.wait()、obj.notify()或obj.notifyAll()时,当前线程必须已经持有obj对象的监视器锁——简单说,你必须在synchronized(obj)的代码块/方法里,才能调用obj的wait/notify方法。
2. 你的代码为什么会出问题?
你的send()和receive()都是静态同步方法,线程进入这些方法时拿的是WaitZero.class的锁,但你却调用了num.wait()和num.notifyAll()——这就犯了两个致命错误:
- 错误一:理论上应该直接抛出
IllegalMonitorStateException:因为当前线程根本没有持有num对象的锁,调用它的wait/notify方法是违反规则的。你说输出不稳定,可能是线程池的异常处理机制让你没看到这个异常(线程池里的线程抛出未捕获异常时,默认不会在控制台打印,除非你设置了未捕获异常处理器)。 - 错误二:即使没抛异常,同步逻辑完全失效:
- 当线程调用
num.wait()时,它会释放的是num对象的锁,但它手里还攥着WaitZero.class的锁——这就导致其他需要进入send()/receive()方法的线程,会因为拿不到WaitZero.class的锁而一直阻塞,最终出现线程执行顺序混乱的情况,比如多个receive线程连续执行,或者send线程无法被唤醒。 - 而当你改成调用
WaitZero.class.wait()和WaitZero.class.notifyAll()时,锁对象和等待的监视器对象一致了:线程持有WaitZero.class的锁,调用wait()会释放这个锁,其他线程可以正常获取锁进入方法;notifyAll()也会唤醒所有等待在WaitZero.class监视器上的线程,这样就能保证send和receive线程严格交替执行,符合你的预期。
- 当线程调用
顺便解答你代码里的两个ToDo
- 什么时候用notify()?:当你能确定只有一个等待线程需要被唤醒时,可以用
notify()。比如你的场景里,理论上每次只有一个send或receive线程在等待,用notify()也能工作,但notifyAll()更安全——因为如果因为某种巧合有多个同类型线程等待,notify()可能会唤醒一个不需要的线程(比如唤醒另一个receive线程而不是send线程),最终导致死锁。 - 什么是monitor(监视器)?:每个Java对象都内置了一个监视器,它相当于一个「锁+等待队列」的组合。当线程获取对象的监视器锁时,其他线程会被阻塞在等待队列里;当线程调用wait()时,会释放锁并进入该对象的等待集合,直到被notify()/notifyAll()唤醒。
synchronized就是用来获取对象监视器锁的机制,wait()/notify()则是用来操作监视器等待集合的工具。
简单来说,synchronized和wait()/notifyAll()必须绑定同一个对象,这样才能让锁机制和等待唤醒机制配合工作。如果对象不匹配,要么直接抛出异常,要么导致线程同步逻辑彻底混乱,出现不可预测的执行结果。
内容的提问来源于stack exchange,提问作者Hearen

