在 Java 并发编程中,调用 Object wait/notify 方法的代码段必须要被包含在 synchronized 块中,接着即是耳熟能详的:调用 wait 方法时,先释放锁,然后线程进入阻塞状态,直至被 notify,然后重新尝试获得锁。看似一气呵成的一顿猛如虎的操作,其个中缘由到底是什么?有些我们看起来理所当然的东西,难道真的是理所当然的吗?本篇文章即谈谈,为什么调用 wait/notify 方法的代码段必须要被包含在 synchronized 块中。
先从 官方文档 入手,官方文档展示了一段代码告诉读者们 wait 方法应如何使用。如果当前线程(即调用 obj.wait 方法的线程)不是 monitor 的 owner,抛出 IllegalMonitorStateException。synchronized 关键字在经过编译后,会在同步代码块的前后生成 monitorenter 和 monitorexit 这两个字节码指令,它们都需要一个 reference 类型的参数指明要锁定和解锁的对象,这也是「monitor」的由来。
synchronized (obj) { while (<condition does not hold>) obj.wait(timeout); ... // Perform action appropriate to condition }
假设要利用 wait/notify 方法实现 BlockingQueue 数据结构,很快的写下了如下代码。
public class BlockingQueue { private Queue<String> buffer = new LinkedList<String>(); public void put(String data) { buffer.add(data); notify(); } public String get() throws InterruptedException { while (buffer.isEmpty()) { wait(); } return buffer.remove(); } }
注意!BlockingQueue 在调用 wait/notify 方法时并没有使用 synchronized 关键字,思考会发生什么问题,考虑以下场景。
产生上述场景所描述问题的关键原因在于 put & notify 和 get & wait 不被原子性(atomic)的调用。很自然的 synchronized 可以发挥保证原子性的作用。但是为了保证原子性,synchronized 完全可以使用任意一个 Object,为什么必须和 wait/notify 方法一致的 Object?其实很简单,结合以下代码:如果使用 BlockingQueue.class 作为锁,当 wait 方法释放 this 锁并进入阻塞状态后,get 方法依旧拥有着 BlockingQueue.class 锁,producer 永远会被阻塞于调用 put 方法,形成死锁。
public class BlockingQueue { private Queue<String> buffer = new LinkedList<String>(); public void put(String data) { synchronized (BlockingQueue.class) { buffer.add(data); notify(); } } public String get() throws InterruptedException { synchronized (BlockingQueue.class) { while (buffer.isEmpty()) { wait(); } return buffer.remove(); } } }
EOF.