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

我们先来看看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 中,然后放弃该对对象的锁。直到发生以下四种情况之一,该线程才会被唤醒:

  1. 其他线程为此对象调用了 notify 方法,并且线程 T 恰好被操作系统选择为要唤醒的线程。
  2. 其他线程为此对象调用了 notifyAll 方法,唤醒了所有线程。
  3. 其他一些线程 interrupt() 中断了线程 T。
  4. 超过了指定的等待时间(当然,如果 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()需要在同步代码块内使用的更多相关文章

  1. 彻底理解线程同步与同步代码块synchronized

    public class Demo { public static synchronized void fun1(){ } public synchronized void fun2(){ } pub ...

  2. About 静态代码块,普通代码块,同步代码块,构造代码块和构造函数的纳闷

    构造函数用于给对象进行初始化,是给与之对应的对象进行初始化,它具有针对性,函数中的一种.特点:1:该函数的名称和所在类的名称相同.2:不需要定义返回值类型.3:该函数没有具体的返回值.记住:所有对象创 ...

  3. -1-5 java 多线程 概念 进程 线程区别联系 java创建线程方式 线程组 线程池概念 线程安全 同步 同步代码块 Lock锁 sleep()和wait()方法的区别 为什么wait(),notify(),notifyAll()等方法都定义在Object类中

     本文关键词: java 多线程 概念 进程 线程区别联系 java创建线程方式 线程组 线程池概念 线程安全 同步 同步代码块 Lock锁  sleep()和wait()方法的区别 为什么wait( ...

  4. 对象及变量的并发访问(同步方法、同步代码块、对class进行加锁、线程死锁)&内部类的基本用法

    主要学习多线程的并发访问,也就是使得线程安全. 同步的单词为synchronized,异步的单词为asynchronized 同步主要就是通过锁的方式实现,一种就是隐式锁,另一种是显示锁Lock,本节 ...

  5. java中的synchronized同步代码块和同步方法的区别

    下面这两段代码有什么区别? //下列两个方法有什么区别 public synchronized void method1(){} public void method2(){ synchronized ...

  6. java 多线程:线程通信-等待通知机制wait和notify方法;(同步代码块synchronized和while循环相互嵌套的差异);管道通信:PipedInputStream;PipedOutputStream;PipedWriter; PipedReader

    1.等待通知机制: 等待通知机制的原理和厨师与服务员的关系很相似: 1,厨师做完一道菜的时间不确定,所以厨师将菜品放到"菜品传递台"上的时间不确定 2,服务员什么时候可以取到菜,必 ...

  7. 0037 Java学习笔记-多线程-同步代码块、同步方法、同步锁

    什么是同步 在上一篇0036 Java学习笔记-多线程-创建线程的三种方式示例代码中,实现Runnable创建多条线程,输出中的结果中会有错误,比如一张票卖了两次,有的票没卖的情况,因为线程对象被多条 ...

  8. 2016/9/25编写java实验报告时对synchronized(同步代码块)的一些感悟

    通过此次实验,明白了多线程的设置和启动.synchronized代码块的用法.线程的优先级使用方法.知道了那几类资源是线程共享的. 我现在理解的多线程是:实例化一个继承了Thread类或实现了Runn ...

  9. Java基础之线程——管理线程同步代码块(BankOperation4)

    控制台程序. 除了同步类对象的方法之外,还可以把程序中的语句或代码块制定为synchronized,这种方式更强大,因为可以指定哪个对象从语句或代码块的同步中获益,而不像同步方法那样仅仅是包含代码的对 ...

  10. Android(java)学习笔记68:同步代码块 和 同步方法 的应用

    1. 同步代码块 和 同步方法 代码示例: (1)目标类,如下: package cn.himi.text; public class SellTicket implements Runnable { ...

随机推荐

  1. Delphi7_VCL线程的使用(一)

    1.TThread类的属性 (1)FreeOnTerminate属性 该属性用于指定当前的线程终止时是否自动删除线程对象.默认值为true. 语法: 1 Property FreeOnTerminat ...

  2. java中list对象不同属性去重合并

    需求:将list中对象的不同属性对应的值去重后,赋值给另一个属性! 实现效果如下图:

  3. Spring之IOC(控制反转)入门理解

    在面向对象编程中,我们经常处理处理的问题就是解耦,程序的耦合性越低表明这个程序的可读性以及可维护性越高(假如程序耦合性过高,改一处代码通常要对其他地方也要做大量修改,难以维护).控制反转(Invers ...

  4. lightgbm与贷款违约预测项目

    lightgbm histogram算法 将连续的浮点值离散成k个离散值,构造宽度为k的histogram leaf-wise生长策略 每次在所有叶子中找到分裂增益最大的一个叶子,一般也是数据量最大的 ...

  5. OpenStack 云主机ping通外网

  6. layui 点击显示与点击隐藏

    主要有lay-filter属性,靠这个属性监听 <div class="layui-col-xs12 layui-col-sm4 layui-col-md4"> < ...

  7. copy file from remote server to local

    scp -r root@IP:/path/to/file(file path on the server) /path/to/filedestination(local path)

  8. jsp第三个作业

    main.jsp <%@ page language="java" import="java.util.*" pageEncoding="utf ...

  9. 1011.Django状态保持以及表单

    一.session保持状态 状态保持: 1. http协议是无状态的:每次请求都是一次新的请求,不会记得之前通信的状态: 2. 客户端与服务器端的一次通信,就是一次会话实现状态保持的方式:在客户端或服 ...

  10. ASP.NET在Repeater中使用Button控件报错

    普通Button在这里会报错,小编找了一天也没有解决这个问题, 这里可以换做LinkButton或者ImageButton替换普通的Button