内容简介:Java语言原生支持的等待-通知机制:
- 在《Java并发 – 死锁》中,通过破坏 占用且等待 条件来规避死锁,核心代码如下
-
while (!allocator.apply(this, target)) {}
-
- 如果apply()操作的时间非常短,并且并发不大,该方案还能应付
- 一旦apply()操作比较耗时,或者并发比较大,该方案就不适用了
- 因为这可能需要循环上万次才能获得锁,非常 消耗CPU
- 最好的方案: 等待-通知
- 当线程要求的条件不满足,则线程 阻塞 自己,进入 等待 状态
- 当线程要求的条件满足后,通知等待的线程,重新开始执行
- 线程阻塞能够避免因循环等待而消耗CPU的问题
- Java语言原生支持 等待-通知机制
- 线程 首先获取互斥锁 ,当线程要求的条件 不满足 时, 释放互斥锁 ,进入等待状态
- 当要求的条件满足时,通知等待的线程, 重新获取互斥锁
等待-通知机制
Java语言原生支持的等待-通知机制: synchronized + wait + notify/notifyAll
wait
- 每个互斥锁有两个独立的等待队列 ,如上图所示,等待队列L和等待队列R
- 左边有一个 等待队列 (等待队列L), 在同一时刻,只允许一个线程进入synchronized保护的临界区
- 当已经有一个线程进入synchronized保护的临界区后,其他线程就只能进入等待队列L进行等待
- 当一个线程 进入临界区后 ,由于某些条件 不满足 ,需要进入 等待 状态,可以调用wait()方法
- 当调用wait()方法后,当前线程就会被 阻塞 ,并且进入到 右边的等待队列 (等待队列R)
- 线程在进入等待队列R的同时,会 释放持有的互斥锁 ,其他线程就有机会获得锁,并进入临界区
- 关键点: sleep不会释放互斥锁
notify/notifyAll
- 当线程要求的条件满足时,可以通过
notify/notifyAll
来通知等待的线程 - 当条件满足时调用notify(),会通知等待队列R( 将等待队列L中第1个节点移到等待队列R )中的线程,告知它 条件曾经满足
- notify()只能保证在通知的时间点,条件是满足的
- 而 被通知线程的执行时间点 与 通知时间点 基本上 不会重合 ,当线程执行的时候,条件很可能已经不满足了
- 被通知的线程如果想要 重新执行 ,仍然需要先获取到 互斥锁
- 因为曾经获取到的锁在调用wait()时已经 释放 了
- 关键点: 执行notify/notifyAll并不会释放互斥锁,在synchronized代码块结束后才真正的释放互斥锁
编码范式
while (条件不满足) { wait(); }
- 可以解决 条件曾经满足 的问题
- 当wait()返回时,条件有可能已经改变了,需要重新检验条件是否满足,如果不满足,继续wait()
notifyAll
- 尽量使用notifyAll()
- notify():会 随机 地通知等待队列R中的 一个 线程
- 隐含动作:先将等待队列L的 第一个节点 移动到等待队列R
- notifyAll():会通知等待队列R中的 所有 线程
- 隐含动作:先将等待队列L的 所有节点 移动到等待队列R(待确定是否正确)
- notify()的风险: 可能导致某些线程永远不会被通知到
- 假设有资源A、B、C、D,线程1~4都对应 同一个 互斥锁L
- 线程1申请到了AB,线程2申请到了CD
- 此时线程3申请AB,会进入互斥锁L的等待队列L,线程4申请CD,也会进入互斥锁L的等待队列L
- 线程1归还AB,通过notify()来通知互斥锁L的等待队列R中的线程,假设为线程4(先被移动到等待队列R)
- 但线程4申请的是CD,不满足条件,执行wait(),而真正该被唤醒的线程3就再也没有机会被唤醒了
等待队列
- wait/notify/notifyAll操作的等待队列都是 互斥锁的等待队列
- 如果synchronized锁定的是this,那么对应的一定是this.wait()/this.notify()/this.notifyAll()
- 如果synchronized锁定的是target,那么对应的一定是target.wait()/target.notify()/target.notifyAll()
- 上面这3个方法能够被调用的前提是 已经获取了相应的互斥锁 ,都必须在synchronized内部被调用
- 如果在synchronized外部调用,或者锁定的是this,而调用的是target.wait(),JVM会抛出IllegalMonitorStateException
public class IllegalMonitorStateExceptionTest { private Object lockA = new Object(); private Object lockB = new Object(); @Test public void test1() throws InterruptedException { // java.lang.IllegalMonitorStateException lockA.wait(); } @Test public void test2() throws InterruptedException { synchronized (lockA) { // java.lang.IllegalMonitorStateException lockB.wait(); } } }
转账实例
Allocator
public class Allocator { private static class Holder { private static Allocator allocator = new Allocator(); } public static Allocator getInstance() { return Holder.allocator; } private Allocator() { } private List<Object> als = new ArrayList<>(); // 一次性申请所有资源 public synchronized void apply(Object from, Object to) { // 编程范式 while (als.contains(from) || als.contains(to)) { try { wait(); // this,单例 } catch (InterruptedException e) { e.printStackTrace(); } } als.add(from); als.add(to); } // 归还资源 public synchronized void free(Object from, Object to) { als.remove(from); als.remove(to); notifyAll(); // this,单例 } }
Account
public class Account { // 必须是单例,因为要分配和释放资源 private Allocator allocator = Allocator.getInstance(); // 账户余额 private int balance; // 转账 public void transfer(Account target, int amt) { // 一次性申请转出账户和转入账户 allocator.apply(this, target); try { // 锁定转出账户 synchronized (this) { // 锁定转入账户 synchronized (target) { if (balance > amt) { balance -= amt; target.balance += amt; } } } } finally { allocator.free(this, target); } } }
转载请注明出处:http://zhongmingmao.me/2019/04/22/java-concurrent-wait-notify/
访问原文「 Java并发 -- 等待-通知机制 」获取最佳阅读体验并参与讨论
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 学习ConcurrentHashMap并发写机制
- HBase - 并发控制机制解析
- 浅析数据库并发控制机制
- Kubernetes ApiServer 并发安全机制
- Java并发编程(02):线程核心机制,基础概念扩展
- 深入解析 PostgreSQL 系列之并发控制与事务机制
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Java语言精粹
Jim Waldo / 王江平 / 电子工业出版社 / 2011-6 / 39.00元
这是一本几乎只讲java优点的书。 Jim Waldo先生是原sun微系统公司实验室的杰出工程师,他亲历并参与了java从技术萌生、发展到崛起的整个过程。在这《java语言精粹》里,jim总结了他所认为的java语言及其环境的诸多精良部分,包括:类型系统、异常处理、包机制、垃圾回收、java虚拟机、javadoc、集合、远程方法调用和并发机制。另外,他还从开发者的角度分析了在java技术周围......一起来看看 《Java语言精粹》 这本书的介绍吧!