youyichannel

志于道,据于德,依于仁,游于艺!

0%

wait / notify / notifyAll 方法的使用事项

三个问题:

  1. 为什么 wait 方法必须在 synchronized 同步代码块中使用?
  2. 为什么 wait / notify / nojtifyAll 被定义在 Object 类中,而 sleep 定义在 Thread 类中?
  3. wait / notifysleep 方法的异同?

为什么 wait 方法必须在 synchronized 同步代码块中使用?

Object#wait 方法注释:

*The current thread must own this object's monitor lock... 
*@apiNote
* The recommended approach to waiting is to check the condition being awaited in
* a {@code while} loop around the call to {@code wait}, as shown in the example
* below. Among other things, this approach avoids problems that can be caused
* by spurious wakeups.
*
* <pre>{@code
* synchronized (obj) {
* while (<condition does not hold> and <timeout not exceeded>) {
* long timeoutMillis = ... ; // recompute timeout values
* int nanos = ... ;
* obj.wait(timeoutMillis, nanos);
* }
* ... // Perform action appropriate to condition or timeout
* }
* }</pre>

在使用 wait 方法时,必须把 wait 方法写在 synchronized 保护的循环代码块中,并且始终判断执行条件是否满足,如果满足就往下继续执行,如果不满足就执行 wait 方法,而在执行 wait 方法之前,必须要先持有对象的 monitor 锁,也就是 synchronized 锁。

为什么要这么设计呢?

现在我们假设,如果不要求 wait 方法放在 synchronized 同步代码块中,而是可以随意调用,那么考虑下面这种场景:

Class Demo {
Queue<String> buf = new LinkedList<>();

public void give(String data) {
buf.add(data);
notify(); // Since someone may be waiting in take
}

public String take() throws InterruptedException {
while(buf.isEmpy()) {
wait();
}
return buf.remove();
}
}

上述代码中,give 方法负责向 buf 中添加数据,添加完之后执行 notify 方法来唤醒之前等待的线程,而 take 方法负责检查整个 buf 是否为空,如果为空,就调用 wait 方法进入等待,如果不为空就取出一个数据,这也就是生产者消费者思想。

但是这段代码没有在 synchronized 同步代码块中,在某些情况下可能因为处理器的调度而出现一些问题:

  1. 首先,消费者线程调用 take 方法并判断 buf.isEmpty() ,若为 true 代表 buf 为空,消费者线程希望进入等待,但是在执行 wait() 方法之前,处理器调度运行了其他线程,当前线程被暂停了,所以此时还没来得及执行 wait() 方法;
  2. 然后,生产者线程开始执行,执行完整个 give 方法过程,它向 buf 中添加了数据,并且执行了 notify 方法,但是 notify 并没有任何效果,因为消费者线程的 wait 方法还没有执行,所以没有在等待中的线程被唤醒;
  3. 此时,刚在被调度器暂停的消费者线程继续执行 wait 方法并进入了等待。

可以看出,虽然消费者判断了 buf 是否为空,但是在真正执行 wait() 方法时,之前判断的结果已经「过期」了,不再符合最新的场景,因为此时「判断-执行」不再是一个原子操作,它在中间被打断,是线程不安全的。

更极端一点,如果此时没有更多的生产者进行生产,消费者可能陷入无休止的等待中。

正是因为 wait() 方法所在的 take() 方法没有被 synchronized 保护,所以 while 判断和 wait 方法无法构成一个原子操作,那么此时整个程序就很容易出现错误的结果。

「使用 synchronized 之后」

Class Demo {
Queue<String> buf = new LinkedList<>();

public void give(String data) {
synchronized(this) {
buf.add(data);
notify(); // Since someone may be waiting in take
}
}

public String take() throws InterruptedException {
synchronized(this) {
while(buf.isEmpy()) {
wait();
}
}
return buf.remove();
}
}

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

除此之外,wait() 方法也会释放 monitor 锁,这也要求线程需要进入到 synchronized 同步代码块内部持有这把锁。

PS:此处可能还存在一个「虚假唤醒」的问题,线程可能在既没有被 notify / notifyAll、也没有被中断或者超时的情况下被唤醒。虽然在实际生产中,虚假唤醒发生的概率很小,但是程序需要保证在发生虚假唤醒时的正确性,所以就需要采用循环的结构:

while(condition does not hold) 
obj.wait();

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

为什么 wait / notify / nojtifyAll 被定义在 Object 类中,而 sleep 定义在 Thread 类中?

这个问题主要有几点原因:

  1. 因为 Java 中每个对象都有一把 monitor 监视器锁,每个对象都可以上锁,于是在对象头中有一块专门用来保存锁信息的位置。这个锁是对象级别的,而非线程级别的,wait / notify / notifyAll 都是对象级别的操作,锁属于对象,所以将这些方法定义在 Object 类中是合适的,因为 Object 是所有对象的父类;
  2. 如果把 wait / notify / notifyAll 定义在 Thread 类中,会带来很大的局限性,比如一个线程可能持有多把锁,以便实现相互配合的复杂逻辑,假设此时 wait() 方法定义在 Thread 类中,如何实现让一个线程持有多把锁呢?又如何明确线程等待的是哪把锁呢?既然是要让当前线程去等待某个对象的锁,自然应该通过操作对象来实现,而不是操作线程;
  3. 因为 sleep() 是让当前线程暂停执行,不涉及到对象类,也不需要获得对象锁。

wait / notify 和 sleep 方法的异同?

主要是对比 wait 和 sleep 方法。

1)相同点:

  • 都可以让线程阻塞;
  • 都可以响应中断,即在阻塞过程中如果收到了中断信号,都可以进行响应,并抛出 InterruptedException 异常。

2)不同点:

  1. wait() 方法必须在 synchronized 同步代码块中执行,sleep() 方法没有这个要求;
  2. 在同步代码块中执行 wait() 方法时,线程会主动释放 monitor 锁,而执行 sleep() 函数不会释放 monitor 锁;
  3. sleep() 方法要求必须定义一个超时时间,时间到期后会主动恢复,而对于没有参数的 wait()方法而言,意味着永久等待,直到被中断或被唤醒才能恢复,它并不会主动恢复;
  4. wait / notify 是 Object 类的方法,而 sleep 是 Thread 类的方法。