Java并发编程实战 05等待-通知机制和活跃性问题
Java并发编程系列
Java并发编程实战 01并发编程的Bug源头
Java并发编程实战 02Java如何解决可见性和有序性问题
Java并发编程实战 03互斥锁 解决原子性问题
Java并发编程实战 04死锁了怎么办
前提
在Java并发编程实战 04死锁了怎么办中,讲到了使用一次性申请所有资源来避免死锁的发生,但是代码中却是使用不断的循环去获取锁资源。如果获取锁资源耗时短、且并发冲突量不大的时候,这个方式还是挺合适的。
如果获取所以资源耗时长且并发冲突量很大的时候,可能会循环上千上万次,这就太消耗CPU了。把上一章的代码贴下来吧。
/** 锁分配器(单例类) */
public class LockAllocator {
private final List<Object> lock = new ArrayList<Object>();
/** 同时申请锁资源 */
public synchronized boolean lock(Object object1, Object object2) {
if (lock.contains(object1) || lock.contains(object2)) {
return false;
}
lock.add(object1);
lock.add(object2);
return true;
}
/** 同时释放资源锁 */
public synchronized void unlock(Object object1, Object object2) {
lock.remove(object1);
lock.remove(object2);
}
}
public class Account {
// 余额
private Long money;
// 锁分配器
private LockAllocator lockAllocator;
public void transfer(Account target, Long money) {
try {
// 循环获取锁,直到获取成功
while (!lockAllocator.lock(this, target)) {
}
synchronized (this){
synchronized (target){
this.money -= money;
if (this.money < 0) {
// throw exception
}
target.money += money;
}
}
} finally {
// 释放锁
lockAllocator.unlock(this, target);
}
}
}
解决这种场景的方案就是使用等待-通知机制。
等待-通知机制
当我们去麦当劳吃汉堡,首先我们需要排队点餐,就如线程抢着获取锁进synchronized同步代码块中。
当我们点完餐后需要等待汉堡完成,所以我们需要等待wait(),因为汉堡还没做好。
当汉堡做好后广播喊了一句“我做好啦!快来领餐”。广播就是notifyAll(),唤醒了所有线程。
然后每个人都过去看看是不是自己的餐。如果不是又进入了等待中。否则就可以拿到汉堡(获取到锁)开吃啦。
当然麦当劳只会说“xx号快来领餐”,我改了一下台词比较好做例子(例子感觉也是一般般,看不懂就看代码吧)。对不起麦当劳了。
在编程领域当中,若线程发现锁资源被其他线程占用了(条件不满足),线程就会进入等待状态wait(释放锁),当其它线程释放锁时,使用notifyAll()唤醒所有等待中的线程。被唤醒的线程就会重新去尝试获取锁。如图:

那么何时等待? 何时唤醒?
何时等待:当线程的要求不满足时等待,在转账的例子当中就是不能同时获取到this和target锁资源时等待。
何时唤醒:当有线程释放锁资源时就唤醒。
修改后的代码如下:
/** 锁分配器(单例类) */
public class LockAllocator {
private final List<Object> lock = new ArrayList<>();
/** 同时申请锁资源 */
public synchronized void lock(Object object1, Object object2) throws InterruptedException {
while (lock.contains(object1) || lock.contains(object2)) {
wait(); // 线程进入等待状态 释放锁
}
lock.add(object1);
lock.add(object2);
}
/** 同时释放资源锁 */
public synchronized void unlock(Object object1, Object object2) {
lock.remove(object1);
lock.remove(object2);
notifyAll(); // 唤醒所有等待中的线程
}
}
public class Account {
// 余额
private Long money;
// 锁分配器
private LockAllocator lockAllocator;
public void transfer(Account target, Long money) throws InterruptedException {
try {
// 获取锁
lockAllocator.lock(this, target);
this.money -= money;
if (this.money < 0) {
// throw exception
}
target.money += money;
} finally {
// 释放锁
lockAllocator.unlock(this, target);
}
}
}
在Account类中,对比上面的代码,我删掉了两层synchronized嵌套,如果涉及到账户余额都先去锁分配器LockAllocator 中获取锁,那么这两层synchronized嵌套其实可以去掉。而且使用wait()和notifyAll()(notify()也是)必须在synchronized代码块中,否则会抛出java.lang.IllegalMonitorStateException`异常。
尽量使用notifyAll
其实使用notify()也可以唤醒线程,但是只会随机抽取一位幸运观众(随机唤醒一个线程)。这样做可能有导致有些线程没那么快被唤醒或者永久都不会有机会被唤醒到。
假如有资源A、B、C、D,线程1申请到AB,线程2申请到CD,线程3申请AB需要等待。此时有线程4申请CD等待,若线程1释放资源时唤醒了线程4,但是线程4还是需要等待线程2释放资源,线程3却没有被唤醒到。
所以除非你已经思考过了使用notify()没问题,否则尽量使用notifyAll()。
notify何时可以使用
notify需要满足以下三个条件才能使用
1.所有等待线程拥有相同的等待条件。
2.所有等待线程被唤醒后,执行相同的操作。
3.只需要唤醒一个线程。
活跃性问题
活跃性问题,指的是某个操作无法再执行下去,死锁就是其中活跃性问题,另外的两种活跃性问题分别为 饥饿 和 活锁
饥饿
在上面的例子当中,我们看到线程3由于无法访问它所需要的资源而不能继续执行时,就发生了“饥饿”,如果在Java应用程序中对线程的优先级使用不当或者在持有锁时执行一些无法结束的结构(无线循环、无限制的等待某个资源),那么也可能发生饥饿。
解决饥饿的问题有三种:1.保证资源充足,2.公平地分配资源,3.避免线程持有锁的时间过长。但是只有方案2比较常用到。在并发编程里,主要是使用公平锁,也就是先来后到的方案,线程等待是有顺序的,不会去争抢资源。这里不展开讲公平锁.
活锁
活锁是另一种活跃性问题,尽管不会阻塞线程,但是也不能继续执行,这就是活锁,因为程序会不断的重复执行相同的操作,而且总是会失败。
就如两个非常有礼貌的人在路上相撞,两个人都非常有礼貌的让到另一边,这样就又相撞了,然后又....,不断地变道,不断地相撞。
在编程领域当中:假如有资源A、B,线程1获取到了资源A的锁,线程2获取到了资源B的锁,此时线程1需要再获取资源B的锁,线程2需要再获取资源A的锁,两个线程获取锁资源失败后释放自己所持有的锁,然后再此重新获取资源锁。这是就又发生了刚才的事情。就这样不断的循环,却又没阻塞。这就是活锁的例子。如图:

解决活锁的问题就是各自等待一个随机的时间再做后续操作。这样同时相撞的概率就很低了。
总结
本文主要讨论了使用等待-通知获取锁来优化不断循环获取锁的机制。若获取锁资源耗时短和并发冲突少则也可以使用不断循环获取锁的机制,否则尽量使用等待-通知获取锁。唤醒线程的方式有notify()和notifyAll(),但是notify()只会随机唤醒一个线程,容易导致线程饥饿,所以尽量使用notifyAll()方式来唤醒线程。
参考文章:
《Java并发编程实战》第10章 活跃性危险
极客时间:Java并发编程实战 06: 用“等待-通知”机制优化循环等待
极客时间:Java并发编程实战 07: 安全性、活跃性以及性能问题
个人博客网址: https://colablog.cn/
如果我的文章帮助到您,可以关注我的微信公众号,第一时间分享文章给您

Java并发编程实战 05等待-通知机制和活跃性问题的更多相关文章
- java并发编程实战《七》安全性、活跃性以及性能问题
安全性.活跃性以及性能问题 安全性问题 那什么是线程安全呢?其实本质上就是正确性,而正确性的含义就是程序按照我们期望的执行,不要让我们感到意外. 存在共享数据并且该数据会发生变化,通俗地讲就是有多个线 ...
- 【Java并发基础】使用“等待—通知”机制优化死锁中占用且等待解决方案
前言 在前篇介绍死锁的文章中,我们破坏等待占用且等待条件时,用了一个死循环来获取两个账本对象. // 一次性申请转出账户和转入账户,直到成功 while(!actr.apply(this, targe ...
- Java并发编程实战 04死锁了怎么办?
Java并发编程文章系列 Java并发编程实战 01并发编程的Bug源头 Java并发编程实战 02Java如何解决可见性和有序性问题 Java并发编程实战 03互斥锁 解决原子性问题 前提 在第三篇 ...
- Java并发编程实战.笔记十一(非阻塞同步机制)
关于非阻塞算法CAS. 比较并交换CAS:CAS包含了3个操作数---需要读写的内存位置V,进行比较的值A和拟写入的新值B.当且仅当V的值等于A时,CAS才会通过原子的方式用新值B来更新V的值,否则不 ...
- 《Java并发编程实战》文摘
更新时间:2017-06-03 <Java并发编程实战>文摘,有兴趣的朋友可以买本纸质书仔细研究下. 一 线程安全性 1.1 什么是线程安全性 当多个线程访问某个类时,不管运行时环境采用何 ...
- 【Java并发编程实战】----- AQS(四):CLH同步队列
在[Java并发编程实战]-–"J.U.C":CLH队列锁提过,AQS里面的CLH队列是CLH同步锁的一种变形.其主要从两方面进行了改造:节点的结构与节点等待机制.在结构上引入了头 ...
- 【Java并发编程实战】-----“J.U.C”:ReentrantReadWriteLock
ReentrantLock实现了标准的互斥操作,也就是说在某一时刻只有有一个线程持有锁.ReentrantLock采用这种独占的保守锁直接,在一定程度上减低了吞吐量.在这种情况下任何的"读/ ...
- 【Java并发编程实战】-----“J.U.C”:Semaphore
信号量Semaphore是一个控制访问多个共享资源的计数器,它本质上是一个"共享锁". Java并发提供了两种加锁模式:共享锁和独占锁.前面LZ介绍的ReentrantLock就是 ...
- 【Java并发编程实战】-----“J.U.C”:ReentrantLock之三unlock方法分析
前篇博客LZ已经分析了ReentrantLock的lock()实现过程,我们了解到lock实现机制有公平锁和非公平锁,两者的主要区别在于公平锁要按照CLH队列等待获取锁,而非公平锁无视CLH队列直接获 ...
随机推荐
- 2020最新的web前端体系和路线图,想学web前端又不知道从哪开始的快来瞧一瞧呀
web前端其实是相对于服务器语言是简单的,并且对于初学者是非常友好的,因为在前期学习能够看到很好的效果.但是他的路线 也就是学习体系不成熟,所以导致很多初学者不知道怎么学?下面我就讲讲web前端的体系 ...
- <vector>常用操作
如果不清楚vector是什么的话就去看我的另一篇随笔吧:https://www.cnblogs.com/buanxu/p/12791785.html 进入正题,vector和string一样,也是一种 ...
- Python带你做个愉快的"动森"玩家! (超简单代码)
最近Switch上的<动物森友会>可谓是炙手可热,它几乎算是任天堂版的<模拟人生>了,它的最新游戏<集合啦!动物森友会>(以下称“动森”)在发售后,取得了不错的媒体 ...
- 【EditPlus】参数设置
1. 设置javac,java快捷键 工具-参数设置-工具-用户工具 组和工具项-组名,更改组名为“java” 添加工具 javac 菜单文字:javac 命令:安装java的javac.exe的绝对 ...
- 【三剑客】awk运算符
1. 算术运算符:+,-,*,/,% [root@oldboy test]# awk 'BEGIN{a=50;b=20;print "(a+b)=",(a+b)}' (a+b)= ...
- Java中的集合Queue
2019独角兽企业重金招聘Python工程师标准>>> package com.zhaogang.test; import org.junit.Test; import java.u ...
- bfs—Dungeon Master—poj2251
Dungeon Master Time Limit: 1000MS Memory Limit: 65536K Total Submissions: 32228 Accepted: 12378 ...
- 安装XCode7.1后,QT5.5出现的各种问题解决方案
安装XCode7.1后,突然发现QT5.5编译不了程序了.直接在终端输入clang,竟然输出如下的信息. Agreeing to the Xcode/iOS license requires admi ...
- JAVA第一次blog总结
JAVA第一次blog总结 0.前言 大一下学期我们开展了OPP这门课程,这也是我们第一次接触到JAVA.与上学期我们在学校里学C语言不同的是,这学期由于疫情原因我们是以网课的方式在学习.在学习中我发 ...
- 最新Idea超实用告别996插件,都是免费
Idea告别996插件 在IntelliJ IDEA中,秉着IDEA自带能实现的快捷方式就不用插件的原则,少用些插件,运行性能也提升一些,虽然很少,哈哈.分享下我个人常用的插件,希望对大家有些帮助.插 ...