You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何synchronized、wait/notifyAll必须作用于同一对象?附问题案例

为什么wait()/notifyAll()与synchronized锁对象不匹配会导致线程同步异常?

我在尝试编写一个结合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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.12 04:55:48