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()唤醒所有等待中的线程。被唤醒的线程就会重新去尝试获取锁。如图:

那么何时等待? 何时唤醒?

何时等待:当线程的要求不满足时等待,在转账的例子当中就是不能同时获取到thistarget锁资源时等待。

何时唤醒:当有线程释放锁资源时就唤醒。

修改后的代码如下:

/** 锁分配器(单例类) */
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等待-通知机制和活跃性问题的更多相关文章

  1. java并发编程实战《七》安全性、活跃性以及性能问题

    安全性.活跃性以及性能问题 安全性问题 那什么是线程安全呢?其实本质上就是正确性,而正确性的含义就是程序按照我们期望的执行,不要让我们感到意外. 存在共享数据并且该数据会发生变化,通俗地讲就是有多个线 ...

  2. 【Java并发基础】使用“等待—通知”机制优化死锁中占用且等待解决方案

    前言 在前篇介绍死锁的文章中,我们破坏等待占用且等待条件时,用了一个死循环来获取两个账本对象. // 一次性申请转出账户和转入账户,直到成功 while(!actr.apply(this, targe ...

  3. Java并发编程实战 04死锁了怎么办?

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

  4. Java并发编程实战.笔记十一(非阻塞同步机制)

    关于非阻塞算法CAS. 比较并交换CAS:CAS包含了3个操作数---需要读写的内存位置V,进行比较的值A和拟写入的新值B.当且仅当V的值等于A时,CAS才会通过原子的方式用新值B来更新V的值,否则不 ...

  5. 《Java并发编程实战》文摘

    更新时间:2017-06-03 <Java并发编程实战>文摘,有兴趣的朋友可以买本纸质书仔细研究下. 一 线程安全性 1.1 什么是线程安全性 当多个线程访问某个类时,不管运行时环境采用何 ...

  6. 【Java并发编程实战】----- AQS(四):CLH同步队列

    在[Java并发编程实战]-–"J.U.C":CLH队列锁提过,AQS里面的CLH队列是CLH同步锁的一种变形.其主要从两方面进行了改造:节点的结构与节点等待机制.在结构上引入了头 ...

  7. 【Java并发编程实战】-----“J.U.C”:ReentrantReadWriteLock

    ReentrantLock实现了标准的互斥操作,也就是说在某一时刻只有有一个线程持有锁.ReentrantLock采用这种独占的保守锁直接,在一定程度上减低了吞吐量.在这种情况下任何的"读/ ...

  8. 【Java并发编程实战】-----“J.U.C”:Semaphore

    信号量Semaphore是一个控制访问多个共享资源的计数器,它本质上是一个"共享锁". Java并发提供了两种加锁模式:共享锁和独占锁.前面LZ介绍的ReentrantLock就是 ...

  9. 【Java并发编程实战】-----“J.U.C”:ReentrantLock之三unlock方法分析

    前篇博客LZ已经分析了ReentrantLock的lock()实现过程,我们了解到lock实现机制有公平锁和非公平锁,两者的主要区别在于公平锁要按照CLH队列等待获取锁,而非公平锁无视CLH队列直接获 ...

随机推荐

  1. 微信小程序 POST传值跳坑

    来源:https://www.cnblogs.com/ordinaryk/p/8430462.html 加这个就行了: header : { 'content-type': 'application/ ...

  2. 哈希Hash定义

    Hash,一般翻译做"散列”,也有直接音译为"哈希"的,就是把任意长度的输入(又叫做预映射, pre-image),通过散列算法,变换成固定长度的输出,该输出就是散列值. ...

  3. Inno Setup, Pascal 字符串带双引号如何写

    Windows 的路径中如果有空格,就需要用双引号括起来.只能填 ASCII-Code-Number (decimal),不能用一般的 escape 方法. # + path + # 查询这个表的第一 ...

  4. 预测球队比赛结果及利用pyinstaller打包文件

    一.预测乒乓球球队比赛成绩 1.乒乓球比赛规则 一局比赛:在一局比赛中,先得11分的一方为胜方:10平后,先多得2分的一方为胜方. 一场比赛:单打的淘汰赛采用七局四胜制,双打淘汰赛和团体赛采用五局三胜 ...

  5. python django mysql配置

    1    django默认支持sqlite,mysql, oracle,postgresql数据库.  <1> sqlite django默认使用sqlite的数据库,默认自带sqlite ...

  6. Vue-cli3.0下的雪碧图插件webpack-spritesmith配置方法

    在前端项目中,为了减少对图片的请求次数,一般而言需要进行雪碧图的配置.即将多张小图标合并成一张图片,这样页面中的小图标都在一张图片上,只需请求一张图片,就可以通过CSS设置各个小图标的显示,利于节省带 ...

  7. 对话Roadstar投资人:一家自动驾驶公司之死(三) ...

    11. Roadstar 如何收场? 雷锋网:你觉得 Roadstar 造成今天这样的局面,是什么导致的? 投资人代表 1:刚才我们也数次表达了,在每个人身上,可能每个人的诉求,不能达到同步,与公司的 ...

  8. 《Python游戏编程快速上手》——1.3 如何使用本书

    本节书摘来自异步社区<Python游戏编程快速上手>一书中的第1章,第1.3节,作者[美] Al Sweigart(斯维加特),李强 译,更多章节内容可以访问云栖社区"异步社区& ...

  9. postman的使用概览

    本文主要描述postman的功能与使用方法Postman是404大厂的基于javascript语言完成的一款超级强大的插件,名字也很亲近(邮递员).可以用于做API请求测试.前端后台测试使用Postm ...

  10. 思维+模拟--POJ 1013 Counterfeit Dollar

    Sally Jones has a dozen Voyageur silver dollars. However, only eleven of the coins are true silver d ...