浅谈Java线程间通信之wait/notify
作者:jingxian 发布时间:2022-06-09 11:26:19
Java中的wait/notify/notifyAll可用来实现线程间通信,是Object类的方法,这三个方法都是native方法,是平台相关的,常用来实现生产者/消费者模式。先来我们来看下相关定义:
wait() :调用该方法的线程进入WATTING状态,只有等待另外线程的通知或中断才会返回,调用wait()方法后,会释放对象的锁。
wait(long):超时等待最多long毫秒,如果没有通知就超时返回。
notify() :通知一个在对象上等待的线程,使其从wait()方法返回,而返回的前提是该线程获取到了对象的锁。
notifyAll():通知所有等待在该对象上的线程。
一个小例子
我们来模拟个简单的例子来说明,我们楼下有个小小的饺子馆,生意火爆,店里有一个厨师,一个服务员,为避免厨师每做好一份,服务员端出去一份,效率太低且浪费体力。现假设厨师每做好10份,服务员就用一个大木盘子端给客户,每天卖够100份就打烊收工,厨师服务员各自回家休息。
思考一下,要实现该功能,如果不使用等待/通知机制,那么最直接的方式可能就是,服务员隔一段时间去厨房看看,满10份就用盘子端出去。
这种方式有两个很大的弊病:
1.如果服务员去厨房看的太勤快,服务员太累了,这样还不如每做一碗就端一碗给客人,大木盘子的作用就体现不出来了。具体表现在实现代码层面就是:需要不断的循环,浪费处理器资源。
2.如果服务员隔很久才去厨房看一下,就无法确保及时性了,可能厨师早都做够10份了,服务员却没观察到。
针对上面这个例子,使用等待/通知机制就合理的多了,厨师每做够10份,就喊一声“饺子好了,可以端走啦”。服务员收到通知,就去厨房将饺子端给客人;厨师还没做够,即还没收到厨师的通知,就可以稍微休息下,但也得竖起耳朵等候厨师的通知。
package ConcurrentTest;
import thread.BlockQueue;
/**
* Created by chengxiao on 2017/6/17.
*/
public class JiaoziDemo {
//创建个共享对象做监视器用
private static Object obj = new Object();
//大木盘子,一盘最多可盛10份饺子,厨师做满10份,服务员就可以端出去了。
private static Integer platter = 0;
//卖出的饺子总量,卖够100份就打烊收工
private static Integer count = 0;
/**
* 厨师
*/
static class Cook implements Runnable{
@Override
public void run() {
while(count<100){
synchronized (obj){
while (platter<10){
platter++;
}
//通知服务员饺子好了,可以端走了
obj.notify();
System.out.println(Thread.currentThread().getName()+"--饺子好啦,厨师休息会儿");
}
try {
//线程睡一会,帮助服务员线程抢到对象锁
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"--打烊收工,厨师回家");
}
}
/**
* 服务员
*/
static class Waiter implements Runnable{
@Override
public void run() {
while(count<100){
synchronized (obj){
//厨师做够10份了,就可以端出去了
while(platter < 10){
try {
System.out.println(Thread.currentThread().getName()+"--饺子还没好,等待厨师通知...");
obj.wait();
BlockQueue
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//饺子端给客人了,盘子清空
platter-=10;
//又卖出去10份。
count+=10;
System.out.println(Thread.currentThread().getName()+"--服务员把饺子端给客人了");
}
}
System.out.println(Thread.currentThread().getName()+"--打烊收工,服务员回家");
}
}
public static void main(String []args){
Thread cookThread = new Thread(new Cook(),"cookThread");
Thread waiterThread = new Thread(new Waiter(),"waiterThread");
cookThread.start();
waiterThread.start();
}
}
运行结果
cookThread--饺子好啦,厨师休息会儿
waiterThread--服务员把饺子端给客人了
waiterThread--饺子还没好,等待厨师通知...
cookThread--饺子好啦,厨师休息会儿
waiterThread--服务员把饺子端给客人了
waiterThread--饺子还没好,等待厨师通知...
cookThread--饺子好啦,厨师休息会儿
waiterThread--服务员把饺子端给客人了
waiterThread--饺子还没好,等待厨师通知...
cookThread--饺子好啦,厨师休息会儿
waiterThread--服务员把饺子端给客人了
waiterThread--饺子还没好,等待厨师通知...
cookThread--饺子好啦,厨师休息会儿
waiterThread--服务员把饺子端给客人了
waiterThread--饺子还没好,等待厨师通知...
cookThread--饺子好啦,厨师休息会儿
waiterThread--服务员把饺子端给客人了
waiterThread--饺子还没好,等待厨师通知...
cookThread--饺子好啦,厨师休息会儿
waiterThread--服务员把饺子端给客人了
waiterThread--饺子还没好,等待厨师通知...
cookThread--饺子好啦,厨师休息会儿
waiterThread--服务员把饺子端给客人了
waiterThread--饺子还没好,等待厨师通知...
cookThread--饺子好啦,厨师休息会儿
waiterThread--服务员把饺子端给客人了
waiterThread--饺子还没好,等待厨师通知...
cookThread--饺子好啦,厨师休息会儿
waiterThread--服务员把饺子端给客人了
waiterThread--打烊收工,服务员回家
cookThread--打烊收工,厨师回家
运行机制
借用《并发编程的艺术》中的一张图来了解下wait/notify的运行机制
可能有人会对所谓监视器(monitor),对象锁(lock)不甚了解,在此简单解释下:
jvm为每一个对象和类都关联一个锁,锁住了一个对象,就是获得了对象相关联的监视器。
只有获取到对象锁,才能拿到监视器,如果获取锁失败了,那么线程就会进入阻塞队列中;如果成功拿到对象锁,也可以使用wait()方法,在监视器上等待,此时会释放锁,并进入等地队列中。
关于锁和监视器的区别,有篇文章写得很详细透彻,在此引用一下,有兴趣的童鞋可以了解一下详谈锁和监视器之间的区别_Java并发
根据上面的图我们来理一下具体的过程
1.首先,waitThread获取对象锁,然后调用wait()方法,此时,wait线程会放弃对象锁,同时进入对象的等待队列WaitQueue中;
2.notifyThread线程抢占到对象锁,执行一些操作后,调用notify()方法,此时会将等待线程waitThread从等待队列WaitQueue中移到同步队列SynchronizedQueue中,waitThread由waitting状态变为blocked状态。需要注意的时,notifyThread此时并不会立即释放锁,它继续运行,把自己剩余的事儿干完之后才会释放锁;
3.waitThread再次获取到对象锁,从wait()方法返回继续执行后续的操作;
4.一个基于等待/通知机制的线程间通信的过程结束。
至于notifyAll则是在第二步中将等待队列中的所有线程移到同步队列中去。
避免踩坑
在使用wait/notify/notifyAll时有一些特别留意的,在此再总结一下:
1.一定在synchronized中使用wait()/notify()/notifyAll(),也就是说一定要先获取锁,这个前面我们讲过,因为只有加锁后,才能获得监视器。否则jvm也会抛出IllegalMonitorStateException异常。
2.使用wait()时,判断线程是否进入wait状态的条件一定要使用while而不要使用if,因为等待线程可能会被错误地唤醒,所以应该使用while循环在等待前等待后都检查唤醒条件是否被满足,保证安全性。
3.notify()或notifyAll()方法调用后,线程不会立即释放锁。调用只会将wait中的线程从等待队列移到同步队列,也就是线程状态从waitting变为blocked;
4.从wait()方法返回的前提是线程重新获得了调用对象的锁。
后记
关于wait/notify的相关内容就介绍到此,在实际使用中,要特别留意上文中提到的几点,不过一般情况下,我们直接使用wait/notify/notifyAll去完成线程间通信,生产者/消费者模型的机会不多,因为Java并发包中已经提供了很多优秀精妙的工具,像各种BlockingQueue等等,后面有机会也会详细介绍的。
猜你喜欢
- 二叉树的深度题目:输入一颗二叉树的根节点,求该树的的深度。输入一颗二叉树的根节点,求该树的深度。从根节点到叶节点依次经过的节点(含根、叶节点
- 我就废话不多说了,大家还是直接看代码吧~Caused by: java.net.SocketException: Software caus
- 在Java中从字符串中删除空格有很多不同的方法,如trim,replaceAll等。但是,在JDK 11添加了一些新的功能,如strip、s
- 一.static关键字使用场景static关键字主要有以下5个使用场景:1.1、静态变量把一个变量声明为静态变量通常基于以下三个目的:作为共
- 被kafka-client和springkafka版本坑上周刚刚欢天喜地的在linux上部了kafka,这周打算用spring-boot框架
- 前言dynamic-tp是一个轻量级的动态线程池插件,它是一个基于配置中心的动态线程池,线程池的参数可以通过配置中心配置进行动态的修改,在配
- 环绕通知:它是spring框架为我们提供的一种可以在代码中手动控制增强部分什么时候执行的方式。问题:当我们配置了环绕通知之后,增强的代码执行
- Atomikos是一个为Java平台提供增值服务的并且开源类事务管理器,如果将事务统一注册到Atomikos中,则可以统一管理。常用于后台管
- 2.4.6 BoxLayout为了简化开发,Swing 引入了 一个新的布局管理器 : BoxLayout 。 BoxLayout 可以在垂
- 因为系统的菜单列表是不轻易改变的,所以不需要在每次请求的时候都去查询数据库,所以,在第一次根据用户id请求到菜单列表的时候,可以把菜单列表的
- 很多核心 Java 面试题来源于多线程(Multi-Threading)和集合框架(Collections Framework),理解核心线
- @Autowired使用构造函数注入public Class Outer { private Inner inner; @Autowired
- renameTo方法public boolean renameTo(File dest),File类中的renameTo方法可以操作文件或目
- 内部类1. 内部类简介(1) 内部类提供了更好的封装,可以把内部类隐藏在外部类之内,不允许同一个包中的其他类访问该类。(2) 内部类成员可以
- spring 多文件配置:1、properties文件2、YAML文件一、properties文件在 Spring Boot 中, 多环境配
- 下面就来分享工具类的内容:使用范围:JavaBean类对象的属性不能是数组、List、Set、Mappublic class MapBean
- 我们都知道mybatis在进行参数判断的时候,直接可以用<if test=""></if> 就可
- 一、什么是过滤器过滤器是对数据进行过滤,预处理过程,当我们访问网站时,有时候会发布一些敏感信息,发完以后有的会用*替代,还有就是登陆权限控制
- 干java 开发这么多年, 之前一直没留意java 进程还区分守护进程和用户进程。守护进程这个概念最早还是在linux系统中接触的,直到近期
- 问题介绍:用二维数组表示一个迷宫,设置迷宫起点和终点,输出迷宫中的一条通路实现思路:二维数组表示迷宫:0表示路且未走过、1表示墙、2表示通路