为什么wait()需要在同步代码块内使用

2023-05-06,,

我们还是通过源代码和代码注释来学习这个问题

我们先来看看wait方法的注释,这里截取最根源的native方法给的注释

Causes the current thread to wait until either another thread invokes the notify() method or the notifyAll() method for this object, or a specified amount of time has elapsed.

大意是调用wait的线程进入等待状态(WAITING和TIME_WAITING),正常情况下有三种机会唤醒,一个是notify随机唤醒第二个是notifyAll(),第三种等待时间混过去

The current thread must own this object's monitor.

This method causes the current thread (call it T) to place itself in the wait set for this object and then to relinquish any and all synchronization claims on this object. Thread T becomes disabled for thread scheduling purposes and lies dormant until one of four things happens:

Some other thread invokes the notify method for this object and thread T happens to be arbitrarily chosen as the thread to be awakened.

Some other thread invokes the notifyAll method for this object.

Some other thread interrupt() interrupts Thread T.

The specified amount of real time has elapsed, more or less. If timeout is zero, however, then real time is not taken into consideration and the thread simply waits until notified.

这一段很重要,我简单翻译一下。

调用 wait 方法的线程必须拥有此对象的监视器。

该方法将当前线程(称为 T)置于此对象的 WaitSet 中,然后放弃该对对象的锁。直到发生以下四种情况之一,该线程才会被唤醒:

    其他线程为此对象调用了 notify 方法,并且线程 T 恰好被操作系统选择为要唤醒的线程。
    其他线程为此对象调用了 notifyAll 方法,唤醒了所有线程。
    其他一些线程 interrupt() 中断了线程 T。
    超过了指定的等待时间(当然,如果 timeout 为零,线程会一直等待直到被通知)。

我们在中简单介绍过上锁原理,本质上就是每个对象的头部都定义一个monitor,也就是这个对象的“控制权”,因此我们在调用wait之前,必须让这个monitor起作用(告诉编译器要使用monitor的意思),就必须使用synchronized中。

我们在看看notifyAll和notify的注解注释

The thread T is then removed from the wait set for this object and re-enabled for thread scheduling. It then competes in the usual manner with other threads for the right to synchronize on the object;

Once it has gained control of the object, all its synchronization claims on the object are restored to the status quo ante - that is, to the situation as of the time that the wait method was invoked. Thread T then returns from the invocation of the wait method. Thus, on return from the wait method, the synchronization state of the object and of thread T is exactly as it was when the wait method was invoked

简单翻译下,即是只要线程T被唤醒(上面说的四种情况),那么这个线程就从WAITSET中移除,再次加入竞争锁的行为中,注意是还是要竞争的!!!一旦又抢到了锁,那么它对对象的所有同步声明都将恢复到调用 wait 方法时的状态,可以接着往下执行。说人话就是,wait让线程等一等,先释放掉拥有的锁,notify/notifyAll就是让凉快地候着的线程重新加入竞争锁的行动中。

在中简单概括为,避免虚假唤醒和长无效唤醒。

我先解释下什么是无效唤醒,这里简单写一下生产者消费者说明下场景(错误的)

生产者消费者伪代码
class Producer{
count++;
notify();
}
class Cosumer{
while(count<=0){
wait();
count--;
}
}

这里试想下这种情况,cpu先运行消费者线程检查count满足≤0即(while(count<=0))这行代码,然后cpu跑去运行生产者线程,运行完notify()唤醒其余线程,但并没有线程wait,然后cpu去运行消费者,消费者从while的下一行继续运行(上下文切换),运行wait(),这时Cosumer就睡觉了,如果后面没有唤醒就一直睡觉了。

再解释下虚假唤醒(Spurious Wakeup),这个词也出现在源码的注解里,这里用金鱼生存场景来模拟一下虚假唤醒这一问题的严重性,这个场景设计要求是金鱼必须吃一次且只能吃一次,一个人喂了金鱼另一个人就不能再喂,金鱼吃完才能继续投喂,这其实就是一个生产者消费者模型

代码多次运行查看结果
package org.joseph.MultiThread.demo;

/**
* @author joseph
* @date 2023/01/30
* @description
**/
public class WaitNotSynchronized { int count=0; public synchronized void produce() throws InterruptedException {
System.out.println(Thread.currentThread().getName()+"进行投喂");
if(count>0){
System.out.println("已经投喂过了,等鱼吃完再投喂!");
this.wait();
}
count++;
System.out.println(Thread.currentThread().getName()+"投喂成功"+",现在食物有"+count+"个");
System.out.println(Thread.currentThread().getName()+"唤醒其余工作者和鱼");
this.notifyAll();
}
public synchronized void consumer() throws InterruptedException {
System.out.println(Thread.currentThread().getName()+"吃东西");
if(count<=0){
System.out.println("没有东西吃,等待投喂者投喂!");
this.wait();
}
count--;
System.out.println(Thread.currentThread().getName()+"吃成功了"+",现在食物有"+count+"个");
System.out.println(Thread.currentThread().getName()+"唤醒其余工作者和鱼");
this.notifyAll();
} public static void main(String[] args) throws Exception{
WaitNotSynchronized waitNotSynchronized = new WaitNotSynchronized();
Runnable feed=new Runnable() {
@Override
public void run() {
for(int i=0;i<10;i++){
try {
waitNotSynchronized.produce();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
Runnable eat=new Runnable() {
@Override
public void run() {
for(int i=0;i<10;i++){
try {
waitNotSynchronized.consumer();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}; Thread thread1 = new Thread(feed, "投喂者1号");
Thread thread2 = new Thread(feed, "投喂者2号");
Thread thread3 = new Thread(eat, "鱼1号");
Thread thread4 = new Thread(eat, "鱼2号");
thread1.start();
thread2.start();
thread3.start();
thread4.start();
} }

理论上水中的食物只有0或者1的存在,但出现了-1,说明出现了问题,我简单解释下,出现-1的场景是由于存在多个生产者和消费者,那么在消费者1号机工作完,会进行唤醒其他线程操作,那么很有可能是消费者2号机被唤醒,注意消费者2号机是由于上一次wait处于阻塞状态,所以消费者2号机会继续运行同步代码剩下的内容!!!

返回看wait的源码注释中这句话“Once it has gained control of the object, all its synchronization claims on the object are restored to the status quo ante - that is, to the situation as of the time that the wait method was invoked.”唤醒后是恢复到调用wait时的状态,也就是说pc计数器中的指针是从wait后代码开始运行,所以为了避免这种问题,if改成while!!!

总结:

wait需要写在synchronized同步块中,理由有两个

1.wait的原理就是获得对象的监听器,而每个对象头部都有monitor的预备,synchronized的原理也是在代码上下添加monitorenter和monitorexit的指令,添加synchronized相当于高速编译器我要是用monitor的功能,没有synchronized,wait就失效了

2.wait的格式最好按照官方文档的要求写,这样的好处避免无效唤醒和虚假唤醒。

为什么wait()需要在同步代码块内使用的相关教程结束。

《为什么wait()需要在同步代码块内使用.doc》

下载本文的Word格式文档,以方便收藏与打印。