前言

在前篇介绍死锁的文章中,我们破坏等待占用且等待条件时,用了一个死循环来获取两个账本对象。

// 一次性申请转出账户和转入账户,直到成功
while(!actr.apply(this, target))
  ;

我们提到过,如果apply()操作耗时非常短,且并发冲突量也不大,这种方案还是可以。否则的话,就可能要循环上万次才可以获取锁,这样的话就太消耗CPU了!

于是我们给出另一个更好的解决方案,等待-通知机制

若是线程要求的条件不满足,则线程阻塞自己,进入等待状态;当线程要求的条件满足时,通知等待的线程重新执行。

Java是支持这种等待-通知机制的,下面我们就来详细介绍这个机制,并用这个机制来优化我们的转账流程。

我们先通过一个就医流程来了解一个完善的“等待-通知”机制。

就医流程—完整的“等待—通知”机制

在医院就医的流程基本是如下这样:

  1. 患者先去挂号,然后到就诊门口分诊,等待叫号;
  2. 当叫到自己的号时,患者就可以找医生就诊;
  3. 就诊过程中,医生可能会让患者去做检查,同时叫一位患者;
  4. 当患者做完检查后,拿着检查单重新分诊,等待叫号;
  5. 当医生再次叫到自己时,患者就再去找医生就诊。

我们将上述过程对应到线程的运行情况:

  1. 患者到就诊门口分诊,类似于线程要去获取互斥锁;
  2. 当患者被叫到号时,类似于线程获取到了锁;
  3. 医生让患者去做检查(缺乏检查报告不能诊断病因),类似于线程要求的条件没有满足;

    患者去做检查,类似于线程进入了等待状态;然后医生叫下一个患者,意味着线程释放了持有的互斥锁;
  4. 患者做完检查,类似于线程要求的条件已经满足;患者拿着检查报告重新分诊,类似于线程需要重新获取互斥锁。

一个完整的“等待—通知”机制如下:

线程首先获取互斥锁,当线程要求条件不满足时,释放互斥锁,进入等待状态;当条件满足时,通知等待的线程,重新获取锁

一定要理解每一个关键点,还需要注意,通知的时候虽然条件满足了,但是不代表该线程再次获取到锁时,条件还是满足的。

Java中“等待—通知”机制的实现

在Java中,等待—通知机制可以有多种实现,这里我们讲解由synchronized配合wait()notify()或者notifyAll()的实现。

如何使线程等待,wait()

当线程进入获取锁进入同步代码块后,若是条件不满足,我们便调用wait()方法使得当前线程被阻塞且释放锁

上图中的等待队列和互斥锁是一一对应的,每个互斥锁都有自己的独立的等待队列(等待队列是同一个)。(这句话还在暗示我们后面唤醒线程时,是唤醒对应锁上的线程。)

如何唤醒线程,notify()/notifyAll()

当条件满足时,我们调用notify()或者notifyAll(),通知等待队列(互斥锁的等待队列)中的线程,告诉它条件曾经满足过

我们要在相应的锁上使用wait() 、notify()和notifyAll()。

需要注意,这三个方法可以被调用的前提是我们已经获取到了相应的互斥锁。所以,我们会发现wait() 、notify() notifyAll()都是在synchronized{...}内部中被调用的。如果在synchronized外部调用,JVM会抛出异常:java.lang.IllegalMonitorStateException。

使用“等待-通知”机制重写转账

我们现在使用“等待—通知”机制来优化上篇的一直循环获取锁的方案。首先我们要清楚如下如下四点:

  1. 互斥锁:账本管理员Allocator是单例,所以我们可以使用this作为互斥锁;
  2. 线程要求的条件:转出账户和转入账户都存在,没有被分配出去;
  3. 何时等待:线程要求的条件不满足则等待;
  4. 何时通知:当有线程归还账户时就通知;

使用“等待—通知”机制时,我们一般会套用一个“范式”,可以看作是前人的经验总结用法。

while(条件不满足) {
wait();
}

这个范式可以解决“条件曾将满足过”这个问题。因为当wait()返回时,条件已经发生变化,使用这种结构就可以检验条件是否还满足。

解决我们的转账问题:

class Allocator {
private List<Object> als;
// 一次性申请所有资源
synchronized void apply(Object from, Object to){
// 经典写法
while(als.contains(from) || als.contains(to)){
// from 或者 to账户被其他线程拥有
try{
wait(); // 条件不满足时阻塞当前线程
}catch(Exception e){
}   
}
als.add(from);
als.add(to);  
}
// 归还资源
synchronized void free(
Object from, Object to){
als.remove(from);
als.remove(to);
notifyAll(); // 归还资源,唤醒其他所有线程
}
}

一些需要注意的问题

sleep()和wait()的区别

sleep()wait()都可以使线程阻塞,但是它们还是有很大的区别:

  1. wait()方法会使当前线程释放锁,而sleep()方法则不会。

    当调用wait()方法后,当前线程会暂停执行,并进入互斥锁的等待队列中,直到有线程调用了notify()或者notifyAll(),等待队列中的线程才会被唤醒,重新竞争锁。

    sleep()方法的调用需要指定等待的时间,它让当前正在执行的线程在指定的时间内暂停执行,进入阻塞状态,但是它不会使线程释放锁,这意味其他线程在当前线程阻塞的时候,是不能进入获取锁,执行同步代码的。
  2. wait()只能在同步方法或者同步代码块中执行,而sleep()可以在任何地方执行。
  3. 使用wait()无需捕获异常,而使用sleep()则必须捕获。
  4. wait()是Object类的方法,而sleep是Thread的方法。

为什么wait()、notify()、notifyAll()是定义在Object中,而不是Thread中?

wait()、notify()以及notifyAll()它们之间的联系是依靠互斥锁,也就同步锁(内置锁),我们前面介绍过,每个Java对象都可以用作一个实现同步的锁,所以这些方法是定义在Object中,而不是Thread中。

小结

“等待—通知”机制是一种非常普遍的线程间协作的方式,我们在理解时可以利用生活中的例子去类似,就如上面的就医流程。上文中没有明显说明notify()和notifyAll()的区别,只是在图中标注了一下。我们建议尽量使用notifyAll(),notify() 是会随机地通知等待队列中的一个线程,在极端情况下可能会使某个线程一直处于阻塞状态不能去竞争获取锁导致线程“饥饿”;而 notifyAll() 会通知等待队列中的所有线程,即所有等待的线程都有机会去获取锁的使用权。

参考:

[1]极客时间专栏王宝令《Java并发编程实战》

[2]Brian Goetz.Tim Peierls. et al.Java并发编程实战[M].北京:机械工业出版社,2016

[3]skywang12345.Java多线程系列--“基础篇”05之 线程等待与唤醒.https://www.cnblogs.com/skywang12345/p/3479224.html

【Java并发基础】使用“等待—通知”机制优化死锁中占用且等待解决方案的更多相关文章

  1. Java并发编程实战 05等待-通知机制和活跃性问题

    Java并发编程系列 Java并发编程实战 01并发编程的Bug源头 Java并发编程实战 02Java如何解决可见性和有序性问题 Java并发编程实战 03互斥锁 解决原子性问题 Java并发编程实 ...

  2. java并发编程实战《六》等待-通知机制

    用"等待-通知"机制优化循环等待 前言 在破坏占用且等待条件的时候,如果转出账本和转入账本不满足同时在文件架上这个条件,就用死循环的方式来循环等待. 1 // 一次性申请转出账户和 ...

  3. Java并发编程(04):线程间通信,等待/通知机制

    本文源码:GitHub·点这里 || GitEE·点这里 一.概念简介 1.线程通信 在操作系统中,线程是个独立的个体,但是在线程执行过程中,如果处理同一个业务逻辑,可能会产生资源争抢,导致并发问题, ...

  4. Java并发编程,Condition的await和signal等待通知机制

    Condition简介 Object类是Java中所有类的父类, 在线程间实现通信的往往会应用到Object的几个方法: wait(),wait(long timeout),wait(long tim ...

  5. Java并发读书笔记:线程通信之等待通知机制

    目录 synchronized 与 volatile 等待/通知机制 等待 通知 面试常问的几个问题 sleep方法和wait方法的区别 关于放弃对象监视器 在并发编程中,保证线程同步,从而实现线程之 ...

  6. 超强图文|并发编程【等待/通知机制】就是这个feel~

    你有一个思想,我有一个思想,我们交换后,一个人就有两个思想 If you can NOT explain it simply, you do NOT understand it well enough ...

  7. java多线程系列(三)---等待通知机制

    等待通知机制 前言:本系列将从零开始讲解java多线程相关的技术,内容参考于<java多线程核心技术>与<java并发编程实战>等相关资料,希望站在巨人的肩膀上,再通过我的理解 ...

  8. 二 Java利用等待/通知机制实现一个线程池

    接着上一篇博客的 一Java线程的等待/通知模型 ,没有看过的建议先看一下.下面我们用等待通知机制来实现一个线程池 线程的任务就以打印一行文本来模拟耗时的任务.主要代码如下: 1  定义一个任务的接口 ...

  9. Java多线程之三volatile与等待通知机制示例

    原子性,可见性与有序性 在多线程中,线程同步的时候一般需要考虑原子性,可见性与有序性 原子性 原子性定义:一个操作或者多个操作在执行过程中要么全部执行完成,要么全部都不执行,不存在执行一部分的情况. ...

随机推荐

  1. GitHub使用详细流程(多人开发)

    联合项目开发GIThub使用 分支 在没有使用分支之前,git会默认有一个分支, 就是主分支(master分支,还记得 git push –u origin master这个命令吗?) 这里的mast ...

  2. Linux 内核 设备结构嵌入

    设备结构包含设备模型核心需要的来模型化系统的信息. 大部分子系统, 但是, 跟踪关于 它们驻留的设备的额外信息. 结果, 对设备很少由空设备结构所代表; 相反, 这个结构, 如同 kobject 结构 ...

  3. ZR提高失恋测2(9.7)

    ZR提高失恋测2(9.7) 网址http://www.zhengruioi.com/contest/392 版权原因,不放题面 A 首先,我们发现对于匹配串\(s\)中所有满足\(s_i \not = ...

  4. 【题解】有标号的DAG计数1

    [HZOI 2015] 有标号的DAG计数 I 设\(f_i\)为\(i\)个点时的DAG图,(不必联通) 考虑如何转移,由于一个DAG必然有至少一个出度为\(0\)的点,所以我们钦定多少个出度为\( ...

  5. 洛谷$P5038\ [SCOI2012]$奇怪的游戏 二分+网络流

    正解:二分+网络流 解题报告: 传送门$QwQ$ 这种什么,"同时增加",长得就挺网络流的$QwQ$?然后看到问至少加多少次,于是考虑加个二分呗?于是就大体确定了做题方向,用的网络 ...

  6. 「CH2501」 矩阵距离 解题报告

    CH2501 矩阵距离 描述 给定一个N行M列的01矩阵 A,\(A[i][j]\) 与 \(A[k][l]\) 之间的曼哈顿距离定义为: \(dist(A[i][j],A[k][l])=|i-k|+ ...

  7. 「算法竞赛进阶指南」0x01 最短Hamilton路径 解题报告

    题目在这里啊题目在这里~ Hamilton路径:将所有点都遍历刚好一次的路径 思路: 数据范围比较小(1~20),所以我们可以考虑暴力中的枚举 数组f[i][j]​ i的二进制表示选取了哪些点 j表示 ...

  8. vue 2.0以上怎么在手机中运行自己的项目

    第一步 打开vue项目 第二步 打开项目config/index.js文件,然后找到 module.exports 配置里面的 dev 配置,修改字段host:0.0.0.0 第三步 打开cmd输入i ...

  9. JavaScript数据类型 - Symbol

    ES5:对象的属性名只能是字符串,当给对象添加新属性时,很容易造成属性名冲突,从而覆盖了原有的属性. ES6:所以ES6中引入了symbol数据类型,他表示独一无二的值,避免了属性名的冲突,此时对象的 ...

  10. Ubuntu管理软件源

    在Ubuntu环境下,我们经常会使用apt-get(apt)命令下载各种软件,当所需软件在官方软件库中找不到时,我们需要添加第三方的软件源,或者由于位于海外的官方软件源下载速度过于感人时,需要添加国内 ...