【Java】为啥 wait 方法必须得在 synchronized 保护的同步代码中使用?|Java 刷题打卡

本文正在参加「Java主题月 – Java 刷题打卡」,详情查看活动链接

为啥 wait 方法必须得在 synchronized 保护的同步代码中使用?

wait 方法的源码注释如下:

# “wait method should always be used in a loop:
 synchronized (obj) {
     while (condition does not hold)
         obj.wait();
     ... // Perform action appropriate to condition
}

# This method should only be called by a thread that is the owner of this object's monitor.”
复制代码

翻译下,即: wait 方法应在 synchronized 保护的 while 代码块中使用,并始终判断执行条件是否满足,如果满足就往下继续执行,如果不满足就执行 wait 方法。在执行 wait 方法之前,必须先持有对象的 monitor 锁,即 synchronized 锁。

为什么这样设计?这样设计又有什么好处?

反向思考,如果不要求 wait 方法放在 synchronized 保护的同步代码中使用,而是可以随意调用,那么就有可能写出这样的代码,如下:

class BlockingQueue {
    Queue<String> buffer = new LinkedList<String>();
    public void offer(String data) {
        buffer.add(data);
        // Since someone may be waiting in take
        notify();  
    }
    
    public String take() throws InterruptedException {
        while (buffer.isEmpty()) {

            wait();
        }
        return buffer.remove();
    }
}
复制代码

在代码中有两个方法:

  1. offer 方法负责往 buffer 中添加数据,添加完之后执行 notify 方法来唤醒之前等待的线程
  2. take 方法负责检查整个 buffer 是否为空,如果为空就进入等待,如果不为空就取出一个数据。

但是这段代码并没有受 synchronized 保护,于是便有可能发生以下场景:

  1. 首先,消费者线程调用 take 方法并判断 buffer.isEmpty 方法是否返回 true,若为 true 代表 buffer 是空的,则线程希望进入等待,但是在线程调用 wait 方法之前,就被调度器暂停了,所以此时还没来得及执行 wait 方法。
  2. 此时生产者开始运行,执行了整个 offer 方法,它往 buffer 中添加了数据,并执行了 notify 方法,但 notify 并没有任何效果,因为消费者线程的 wait 方法没来得及执行,所以没有线程在等待被唤醒。
  3. 此时,刚才被调度器暂停的消费者线程回来继续执行 wait 方法并进入了等待。

把代码改写成源码注释所要求的被 synchronized 保护的同步代码块的形式,代码如下:

public void offer(String data) {
   synchronized (this) {
      buffer.add(data);
      notify();
  }
}

public String take() throws InterruptedException {
   synchronized (this) {
    while (buffer.isEmpty()) {
         wait();
       }
     return buffer.remove();
  }
}
复制代码

这样就可以确保 notify 方法永远不会在 buffer.isEmptywait 方法之间被调用,提升了程序的安全性。

另外,wait 方法会释放 monitor 锁,这也要求必须首先进入到 synchronized 内持有这把锁。

这里还存在一个“虚假唤醒”(spurious wakeup)的问题,线程可能在既没有被 notify/notifyAll,也没有被中断或者超时的情况下被唤醒,这种唤醒是不希望看到的。虽然在实际生产中,虚假唤醒发生的概率很小,但是程序依然需要保证在发生虚假唤醒的时候的正确性,所以就需要采用 while 循环的结构。

while (condition does not hold)
    obj.wait();
复制代码

这样即便被虚假唤醒了,也会再次检查 while 里面的条件,如果不满足条件,就会继续 wait,也就消除了虚假唤醒的风险。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享