为什么wait()需要在同步代码块内使用
我们还是通过源代码和代码注释来学习这个问题
我们先来看看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()需要在同步代码块内使用的更多相关文章
- 彻底理解线程同步与同步代码块synchronized
public class Demo { public static synchronized void fun1(){ } public synchronized void fun2(){ } pub ...
- About 静态代码块,普通代码块,同步代码块,构造代码块和构造函数的纳闷
构造函数用于给对象进行初始化,是给与之对应的对象进行初始化,它具有针对性,函数中的一种.特点:1:该函数的名称和所在类的名称相同.2:不需要定义返回值类型.3:该函数没有具体的返回值.记住:所有对象创 ...
- -1-5 java 多线程 概念 进程 线程区别联系 java创建线程方式 线程组 线程池概念 线程安全 同步 同步代码块 Lock锁 sleep()和wait()方法的区别 为什么wait(),notify(),notifyAll()等方法都定义在Object类中
本文关键词: java 多线程 概念 进程 线程区别联系 java创建线程方式 线程组 线程池概念 线程安全 同步 同步代码块 Lock锁 sleep()和wait()方法的区别 为什么wait( ...
- 对象及变量的并发访问(同步方法、同步代码块、对class进行加锁、线程死锁)&内部类的基本用法
主要学习多线程的并发访问,也就是使得线程安全. 同步的单词为synchronized,异步的单词为asynchronized 同步主要就是通过锁的方式实现,一种就是隐式锁,另一种是显示锁Lock,本节 ...
- java中的synchronized同步代码块和同步方法的区别
下面这两段代码有什么区别? //下列两个方法有什么区别 public synchronized void method1(){} public void method2(){ synchronized ...
- java 多线程:线程通信-等待通知机制wait和notify方法;(同步代码块synchronized和while循环相互嵌套的差异);管道通信:PipedInputStream;PipedOutputStream;PipedWriter; PipedReader
1.等待通知机制: 等待通知机制的原理和厨师与服务员的关系很相似: 1,厨师做完一道菜的时间不确定,所以厨师将菜品放到"菜品传递台"上的时间不确定 2,服务员什么时候可以取到菜,必 ...
- 0037 Java学习笔记-多线程-同步代码块、同步方法、同步锁
什么是同步 在上一篇0036 Java学习笔记-多线程-创建线程的三种方式示例代码中,实现Runnable创建多条线程,输出中的结果中会有错误,比如一张票卖了两次,有的票没卖的情况,因为线程对象被多条 ...
- 2016/9/25编写java实验报告时对synchronized(同步代码块)的一些感悟
通过此次实验,明白了多线程的设置和启动.synchronized代码块的用法.线程的优先级使用方法.知道了那几类资源是线程共享的. 我现在理解的多线程是:实例化一个继承了Thread类或实现了Runn ...
- Java基础之线程——管理线程同步代码块(BankOperation4)
控制台程序. 除了同步类对象的方法之外,还可以把程序中的语句或代码块制定为synchronized,这种方式更强大,因为可以指定哪个对象从语句或代码块的同步中获益,而不像同步方法那样仅仅是包含代码的对 ...
- Android(java)学习笔记68:同步代码块 和 同步方法 的应用
1. 同步代码块 和 同步方法 代码示例: (1)目标类,如下: package cn.himi.text; public class SellTicket implements Runnable { ...
随机推荐
- STM32F103RCT6驱动AD7705(cubeide)
首先在cubeide上配置spi,使用spi1,由于正点开发板上的nr24l01与ad7705正好相同,因此根据引脚配置PA1为DRDY,PC4为CS片选 根据手册上所写,配置寄存器初始化 uint8 ...
- 导航条透明,ios11系统,会出现偏移64的问题
在当前页面加入下面方法 - (void)viewWillAppear:(BOOL)animated{ [super viewWillAppear:animated]; [self.navigation ...
- 廖雪峰JS知识点整理——快速入门
基本语法 1.每个语句以:结尾. 2.单行注释://... 3.多行注释:/*... ...*/ 数据类型和变量 运算 1.==自动转换数据类型在比较,不推荐使用 2.===不会转换数据类型,推荐使用 ...
- js 原生数据类型判断
之前一直使用的jquery的数据类型判断,比如:isArray()等,今天看到了一个判断数据类型的简单的原生方法,分享给大家 Object.prototype.toString 方法返回对象的类型字符 ...
- 一键搭建dns
#!/bin/bash DOMAIN=wang.orgHOST=wwwHOST_IP=10.0.0.100LOCALHOST=`hostname -I | awk '{print $1}'` . /e ...
- Vuex----Actions
Actions用于处理异步任务. 如果通过异步操作变更数据,必须通过 Action,而不能使用Mutation,但是在 Action中还是要通过触发Mutation的方式间接变更数据. 注意: 在Ac ...
- PageHeplper使用
1.引入POM 1 <dependency> 2 <groupId>com.github.pagehelper</groupId> 3 <artifactId ...
- vue移动端登录与登录保持
成品效果 首先进入首页点击右下角个人中心,若状态为登录中则进入个人中心页面,否则进入登录页 实现步骤 首先完成静态的登录页与个人中心页面 登录页 <template> <div cl ...
- 三、JMeter实战-快速上手JMeter
1.JMeter基本操作 线JMeter最基本的操作有三个步骤: 先添加一个线程组. 添加HTTP请求. 添加查看结果树. 1.1.添加线程组 在测试计划下新建一个线程组 1.2.添加HTTP请求 在 ...
- AT ARC092F Two Faced Edges
题意:给定一个有向图,保证无重边自环,求将图中的每条边反向后强联通分量的个数是否会改变. 数据范围:$n$ $≤$ $1e3$,$m$ $≤$ $2e5$. 首先考虑一条边的影响. 因为一条边只能连接 ...