文章目录

  • 纵向学习
    • 概念
    • 使用
    • 原理
    • 实战
      • Object.wait()/notify()
      • Object.wait(long)/notify()
    • 问题
      • 过早唤醒
      • 信号丢失
      • 欺骗性唤醒
      • 上下文切换
  • 横向学习
    • Thread.join()
    • 条件变量
      • 概念
      • 使用

纵向学习

概念

多线程环境中,共享条件未满足当前线程的执行,这可能是暂时的,之后其他线程会更新共享条件从而使其成立,因此我们可以将当前线程暂停,直到条件满足,这个过程依赖于wait、notify;
等待(Wait):一个线程因执行目标动作所需的条件未满足从而被暂停的过程被称为等待;
通知(Notify):一个线程更新了共享变量的状态,使其他线程满足执行目标动作的条件,从而唤醒被暂停的线程,这个过程被成为通知。
在java中,任何对象都可以实现等待和通知。

使用

wait()
Object.wait()的作用使其执行的线程被暂停(生命状态变为WAITING),Object.wait()执行的线程被成为等待线程。
我们先看Object.wait()的伪代码:

   //原子操作,在调用之前需要新获取内部锁synchronized (someObject){while(保护条件不成立){//调用someObject来暂停当前线程someObject.wait()}//其它线程执行了notify,使条件满足时,执行目标动作doAction();}

其中,保护条件时一个共享变量条件表达式,返回值为布尔类型,当保护条件不成立,会执行wait()方法。一个线程只有在持有一个对象的内部锁的情况下才能够调用该对象的wait()方法,因此someObject.wait()一定要放到临界区当中。上面的伪代码就是受保护的方法,其中包含了3个条件:保护条件(共享变量布尔表达式)、暂停当前线程和目标动作。

由于一个对象的someObject.wait()方法可以被多个线程执行,因此一个对象可能存在多个等待线程。someObject上的等待线程可以通过其他线程执行someObject.notify()来唤醒。在线程执行someObject.wait()方法时会释放锁(目的是为了防止锁泄漏),但该方法并未执行结束。其它线程执行someObject.notify()时会随机唤醒一个线程,被唤醒的线程并不会立刻结束wait()方法,而是需要再次申请someObject对象的内部锁,当持有锁的时候才会执行someObject.wait()剩余的指令,直到方法返回(这里看不懂没关系,下面有它的原理讲解)。
注意事项:
1、一定要用synchronized来修饰:为了保证等待线程对保护条件的判断以及目标动作的执行是一个原子操作,因为在保护条件判断成立后、目标动作执行前可能有其他线程对共享变量更新,使保护条件重新不成立,导致执行结果未符合预期。
2、一定要使用while循环:当其他线程更新保护条件并执行someObject.notify()时,等待线程被唤醒,在运行到持有该对象内部锁的这段时间内,又有其它线程更新了保护条件使其又不成立;所以,需要被等待线程再次判断保护条件是否真的成立。
3、当前线程执行Object.wait()释放的锁只是该wait方法所属对象的内部锁,当前线程所持有其它对象的锁并不会被释放。

notify()
Object.notify()的作用是随机唤醒一个被暂停的线程,Object.notify()的执行线程被称为通知线程。
Object.notify()的伪代码如下:

synchronized (someObject){//更新保护条件updateSharedState();//唤醒其他线程someObject.notify();}

上面的代码被成为通知方法;它包含了2个要素:更新共享变量,唤醒其他线程。同wait方法,只有线程持有内部锁的情况下才能执行notify方法,因此,someObject.notify()总是放在临界区当中。也正是因为这样,所以线程执行完someObject.wait()方法后需要释放内部锁,否则通知线程无法进入临界区,无法执行notify。
注意事项:
1、唤醒线程执行完someObject.notify()方法之后并不会释放内部锁,因此,为了让等待线程可以快速的重新获取锁,要尽量将唤醒线程的someObject.notify()方法放到靠近临界区结束的位置。
2、唤醒线程执行完someObject.notify()方法后,只会随机唤醒一个等待线程,有可能并不是我们期望唤醒的那个线程,所以可以使用someObject.notifyAll()来唤醒该对象上的所有等待线程。

原理

java虚拟机会为每个对象维护一个称为等待集(Wait Set)的队列,该队列用于存储该对象上的等待线程。当前线程执行Object.wait()方法时会将自己加入到该对象的等待集中,并释放当前对象的内部锁,当其它线程执行Object.nofity()方法时,会随机唤醒等待集中的一个线程,被唤醒的线程仍然会停留在当前对象的等待集中(Object.wait()方法尚未执行结束),直到被唤醒的线程再次获取到内部锁,jvm才会把当前线程从等待集中移除,之后Object.wait()方法才执行结束。
Object.wait()的伪代码如下:

   public void wait() {//当前线程必须持有当前对象的内部锁if (!Thread.holdsLock(this)) {throw new IllegalMonitorStateException();}if (当前对象不在等待集中){//将当前线程加入到当前对象的等待集中addToWaitSet(Thread.currentThread());}//原子操作atomic{//释放当前对象的内部锁releaseLock(this);//暂停当前线程block(Thread.currentThread());//语句1}//再次申请当前对象的内部锁acquireLock(this);//语句2//将当前线程从当前对象等待集中移除removeFromWaitSet(Thread.currentThread());//这时候再返回return;}

实战

Object.wait()/notify()

现在用wait、notify来演示一个demo(可将代码粘贴到本地便于理解),场景如下:
现在有两个系统,一个告警系统,一个告警接收系统;前者负责发送告警信息给后者。该代码主要有三个线程来实现的,一个线程用来与告警接收系统建立网络连接;一个线程是心跳线程,负责监听与告警接收系统的连接情况;还有一个线程负责发送告警信息给告警接收系统(执行sendAlarm方法)

public class AlarmAgent {private final static AlarmAgent INSTANCE = new AlarmAgent();private Object lock = new Object();//是否可以连接到告警服务器private boolean connectedToServer = false;//心跳线程,用于检测告警代理与告警服务器的网络连接是否正常private final HeartBeatThread heartBeatThread = new HeartBeatThread();private AlarmAgent() {}public static AlarmAgent getInstance() {return INSTANCE;}@PostConstructpublic void init() {connectToServer();heartBeatThread.setDaemon(true);heartBeatThread.start();}public void connectToServer() {//创建启动网络连接线程,该线程中与告警服务器建立连接new Thread(() -> {doConnect();}).start();}private void doConnect() {//模拟实际操作耗时try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (this) {connectedToServer = true;//连接建立完毕,通知以唤醒告警发送线程lock.notify();}}//被调用的方法public void sendAlarm(String message) throws InterruptedException {synchronized (this) {while (!connectedToServer) {System.out.println("Alarm was not connected to server");lock.wait();}//将告警消息上报到告警服务器doSendAlarm(message);}}private void doSendAlarm(String message) {System.out.printf("Alarm sent %s", message);}class HeartBeatThread extends Thread {@SneakyThrows@Overridepublic void run() {Thread.sleep(1000);while (true) {if (checkConection()) {connectedToServer = true;} else {connectedToServer = false;System.out.println("Alarm was disconnected from server");//检测到连接中断,重新建立连接connectToServer();}Thread.sleep(2000);}}}//检测告警与服务器的连接情况private boolean checkConection() {boolean isConnected = true;final Random random = new Random();//模拟随机性的网络连接int rand = random.nextInt(1000);if (rand < 500) {isConnected = false;}return isConnected;}}

connectedToServer 变量表示告警和告警接收系统的连接状态,在发送告警(sendAlarm方法)时,要检测连接状态,如果connectedToServer为false,表示连接失败,则进行wait;当心跳线程判断连接失败的时候,会执行connectToServer()方法,修复成功后,进行nofity,这时发送告警的线程就可以继续执行doSendAlarm方法。

Object.wait(long)/notify()

Object.wait()方法会一直等待下去,直到被唤醒;Object.wait(long)方法可以指定一个超时时间,在这期间未被唤醒的话,则java虚拟机会自动唤醒该线程。但是要区分是等待超时而结束还是由其他线程主动唤醒而结束的,需要进行额外的处理。可以参考如下wait(long)方法的使用:

public class TimedWaitNotify {private final static Object lock = new Object();private static boolean ready = false;protected final static Random random = new Random();public static void main(String[] args) throws InterruptedException {Thread thread = new Thread(() -> {for (; ; ) {synchronized (lock) {ready = random.nextInt(100) < 50 ? true : false;if (ready) {lock.notify();}}try {//模拟继续执行任务Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}}});thread.setDaemon(true);thread.start();waiter(1000);}public static void waiter(final long timeOut) throws InterruptedException {if (timeOut < 0) {throw new IllegalArgumentException();}long start = System.currentTimeMillis();long waitTime;long now;synchronized (lock) {while (!ready) {now = System.currentTimeMillis();//计算剩余等待时间waitTime = timeOut - (now - start);if (waitTime < 0) {//执行超时break;}//非执行超时,继续等待lock.wait(waitTime);}if (ready) {//表明是被notify唤醒的,继续执行要做的内容} else {//等待了 waitTime,但还是没有notify被唤醒,则证明是等待超时}}}
}

在上述执行wait(long)方法之前,要先计算出该程序的执行时间,然后用等待的时间减去前者,得到的结果才是最终需要等待的时间。如果等待了这些时间还没有被唤醒,则说明是等待超时了。

问题

wait、notify的使用不当会带来如下问题:

过早唤醒

假设T1、T2两个线程使用不同的保护条件,但都是someObject对象上的线程,当T1、T2都进行someObject.wait()的时候,线程T3执行了T1的保护条件使其成立,但执行了someObject.notifyAll(),导致T2也被唤醒,当T2被唤醒之后,发现自己的条件并未满足,于是再次进行等待。这种等待线程因所需保护条件并未成立而被唤醒的现象叫做过早唤醒。过早唤醒会造成资源的浪费。
解决方法可使用Condition(下文介绍),或者使用不同的对象锁。

信号丢失

如果等待线程没有进行保护条件的判断,直接执行了someObject.wait()方法,但通知线程在等待线程执行someObject.wait()方法之前已经执行过了someObject.notify(),对等待线程来说,丢失了一个被唤醒的信号,导致等待线程永远等待下去,因此被称为信号丢失。信号丢失还有一种体现就是多个等待线程共用同一个保护条件,但是通知线程只执行了notify,未执行notifyAll,因此只会随机唤醒一个等待线程,其他线程将继续等待。
解决方法是等待线程的保护条件放在while循环当中进行判断。

欺骗性唤醒

等待线程有可能在没有任何线程执行notify或notifyAll方法的情况下被唤醒。这种现象被成为欺骗性唤醒。这种概率非常低,导致原因是java平台对操作系统的妥协。
解决方法和信号丢失的解决方法一致,将保护条件放到while循环中。

上下文切换

等待线程在完整的执行完wait方法后,会进行至少两次锁的申请与释放,第一次是进入临界区和退出临界区,第二次是在执行wait方法时先释放锁,通知线程执行notify时,等待线程需要再次申请锁;一共两次。这时如果等待线程并没有申请到锁,就会再次阻塞,从而导致了上下文切换。其次如果遇到过早唤醒的现象,也会增加额外的上下文切换。
通知线程要完整的执行notify方法也需要一次锁的申请与释放。
减少上下文切换的方法之一是使用notify来代替notifyAll,减少过早唤醒的现象。另外把通知线程的notify和notifyAll方法放到退出临界区的最后代码部分,保证通知完成可以立即释放锁,让等待线程快速拿到锁。

横向学习

Thread.join()

Thread.join()方法也可以实现线程的等待,并且可以通过Thread.join(long time) 来进行指定时间的等待,与wait、notify方法一样。但实际上,Thread.join(long time)是通过wait、notify来实现的。

 public final synchronized void join(long millis)throws InterruptedException {long base = System.currentTimeMillis();long now = 0;if (millis < 0) {throw new IllegalArgumentException("timeout value is negative");}if (millis == 0) {while (isAlive()) {wait(0);}} else {while (isAlive()) {long delay = millis - now;if (delay <= 0) {break;}//等待线程执行wait方法,目标线程run方法执行结束会执行notify方法wait(delay);now = System.currentTimeMillis() - base;}}}

条件变量

概念

由于wait、notify比较底层,操作不当容易出现过早唤醒等问题,因此,在工作中,我们尽量使用Condition可以替代wait、notify的使用。但经过上文对wait、notify的学习,可以有效帮助我们对Condition的理解,其使用和wait、nofity基本一致。
Lock.newCondition()的返回值就是一个Condition实例,因此可以使用任意一个显示锁进行调用,并使用Condition.await/signal代替了上文的Object.wait/notify,Condition的实例就被成为条件变量

使用

每一个Condition实例内部都有一个存储等待线程的等待队列,设cond1和cond2是两个不同的Condition实例,cond1执行await方法时,会进入等待队列中,通知线程执行cond.signal时,只会唤醒cond1中的任意一个线程,并不会影响cond2中等待的线程,即使执行了cond1.singalAll也不会影响,从而解决了过早唤醒的问题。
condition.await/singal的使用和wait/notify基本上是一致的,它的使用模板如下:

public class ConditionDemo {private final Lock lock = new ReentrantLock();private Condition condition = lock.newCondition();public void testMethod() throws InterruptedException {lock.lock();try {while (保护条件不成立) {condition.await();}} finally {lock.unlock();}//do something}public void notifyMethod(){lock.lock();try {//更新了条件变量changeConditon();condition.notify();} finally {lock.unlock();}}
}

主要区别就是将wait/notify的内部锁换成了显示锁。
上面说到Condition可以解决过早唤醒的问题,解决方法为创建不同的条件变量。除此之外,它的awaitUtil(Date)可以区分出等待线程的唤醒由于等待超时还是主动唤醒引起的。
下面是它的使用demo:

public class ConditionTimeWait {private final static Lock lock = new ReentrantLock();private static Condition condition = lock.newCondition();private static boolean ready = false;protected final static Random random = new Random();public static void main(String[] args) throws InterruptedException {Thread thread = new Thread(() -> {lock.lock();try {ready = random.nextInt(100) < 50 ? true : false;if (ready) {condition.signal();}} finally {lock.unlock();}try {//模拟继续执行任务Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}});thread.setDaemon(true);thread.start();waiter(1000);}public static void waiter(final long timeOut) throws InterruptedException {if (timeOut < 0) {throw new IllegalArgumentException();}//最终的等待时间Date deadLineDate = new Date(System.currentTimeMillis() + timeOut);boolean continueToWait = true;lock.lock();try {while (!ready) {if (!continueToWait) {return;}//返回为true说明是被唤醒的,否则是超时等待。continueToWait = condition.awaitUntil(deadLineDate);}doSomething();} finally {lock.unlock();}}
}

condition.awaitUntil(Data)方法返回为true就是就是被唤醒的,如果返回为false表示是超时等待引起的。
感谢您的观看,欢迎评论,一起探讨~

一篇文章带你彻底搞懂wait/notify相关推荐

  1. 一篇文章带你彻底搞懂join的用法

    java多线程里的join,从字面意思来看是联合,合并的意思,但如果面试时这么回答,基本上可以断定面试者还没搞懂.join究竟能干什么,今天给出一个最通俗的解释,那就是在多线程环境下实现暂时以单线程执 ...

  2. hashmap为什么用红黑树_关于HashMap的实现,一篇文章带你彻底搞懂,再也不用担心被欺负

    推荐学习 刷透近200道数据结构与算法,成功加冕"题王",挤进梦中的字节 面试官杠上Spring是种什么体验?莫慌,送你一套面试/大纲/源码 前言 在介绍HashMap之前先了解一 ...

  3. 一篇文章带你彻底搞懂·比特币的相关知识

  4. js等待 callback 执行完毕_前端开发,一篇文章让你彻底搞懂,什么是JavaScript执行机制!...

    不论你是javascript新手还是老鸟,不论是面试求职,还是日常开发工作,我们经常会遇到这样的情况:给定的几行代码,我们需要知道其输出内容和顺序.因为javascript是一门单线程语言,所以我们可 ...

  5. 六大维度层层剖析,一篇文章带你快速读懂信息无障碍

    我的同事和朋友有一部分是视障人士--盲人或者低视力. 这类群体,据中国盲人协会最新统计在中国有1700多万,加上病变.意外.功能性退化,视障人群比例大约是100比1,这个比例其实很高. 我的这些朋友习 ...

  6. 间隔拍摄怎么玩?一篇文章让你彻底搞懂

    目前很多相机中都有间隔拍摄功能,这个功能怎么使用,又能拍摄出什么效果呢?废话不说,下面就为大家进行讲解间隔拍摄的作用 间隔拍摄的作用在于将一些列拍摄参数输入到相机中后可以使相机在指定时间内自动进行指定 ...

  7. 入门、复习微服务的同学看过来,一篇文章让你彻底搞懂微服务

    废话不多说,各位先看下微服务的简介 微服务简介 微服务是一种经过良好架构设计的分布式架构方案,微服务架构特征: 单一职责:微服务拆分粒度更小,每一个服务都对应唯一的业务能力,做到单一职责,避免重复业务 ...

  8. 一篇文章带你搞懂网络层(网际层)-- 地址篇

    网络层(Network Layer)是OSI模型中的第三层(TCP/IP模型中的网际层),提供路由和寻址的功能,使两终端系统能够互连且决定最佳路径,并具有一定的拥塞控制和流量控制的能力.相当于发送邮件 ...

  9. 一篇文章带你搞懂微信小程序的开发过程

    点击上方"前端进阶学习交流",进行关注 回复"前端"即可获赠前端相关学习资料 今 日 鸡 汤 只解沙场为国死,何须马革裹尸还. 大家好,我进阶学习者. 前言 小 ...

最新文章

  1. 视频|结构光编码与三维重建
  2. 深入理解javascript异步编程障眼法h5 web worker实现多线程
  3. 测试的目的_为什么需要测试?(软件测试的目的)
  4. html居右显示语言设置,iOS开发:纯代码设置UIButton文字居左或者居右显示
  5. java 邮件跟踪_如何跟踪邮件已读状态(Java)
  6. 速来!视觉算法大奖赛,奖品丰厚、项目接地气!
  7. java打印前线程的id_logback打印日志输出线程ID:切面模式
  8. php声明js变量类型,js中变量是什么以及有哪些类型
  9. 常用的排序算法总结(一)
  10. Java各层之间的关系
  11. java tomcat数据库连接池,tomcat 数据库连接池拿不到连接
  12. ubuntu 解析控制 PS4手柄
  13. [gdc17]《守望先锋》的EntityComponent架构
  14. Kali 无线网卡无法连接到网络
  15. linux生成秘钥库,在Linux中,生成强预共享密钥(PSK )的4种方法
  16. 商城电商day 06 三、商品详情业务需求分析
  17. 小白IT:从0~明白带你体验python中做上帝感觉--一切皆对象,处处是多态——面向对象
  18. 在VMware中安装CentOS7(超详细的图文教程)
  19. 局域网安全之ARP攻击
  20. 通过tushare的股票数据绘制股票各曲线图——周K线

热门文章

  1. 【SQL查询日志】查看数据库历史查询记录
  2. 【C/C++】gcc与g++
  3. 1G到5G之争:一部30年惊心动魄的移动通信史
  4. ISO26262 汽车功能安全资料汇总(1)-失效率
  5. onchange监听input值变化及input隐藏后change事件不触发的原因与解决方法
  6. 剑侠情缘微信539服务器,《新剑侠情缘》6月24日版本更新开服公告
  7. A Love Letter To Josephine
  8. 百度ERNIE新突破 登顶中文医疗信息处理权威榜单CBLUE冠军
  9. 【本地网络服务器】(一)Windows安装CentOS双系统
  10. Zstack EPICS Archiver在小课题组的使用经验