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. nginx history路由模式时,页面返回404重定向index.html

    1.路由默认是带#的,有时我们感觉不美观,就使其变为history模式,也就没有#字符 2.# 如果找不到当前页面(404),就返回index.html,重新分配路由 location ^~/prod ...

  2. 新的知识点来了-ES6 Proxy代理 和 去银行存款有什么关系?

    ES给开发者提供了一个新特性:Proxy,就是代理的意思.也就是我们这一节要介绍的知识点. 以前,ATM还没有那么流行的时候(暴露年纪),我们去银行存款或者取款的时候,需要在柜台前排队,等柜台工作人员 ...

  3. RabbitMQ Hello world(二)

    简介: Rabbitmq 是消息代理中间件,它接收或者发送消息.你可以把它想想宬一个邮局:当你把邮件放到邮箱时,你可以确定某一位邮递员可以准确的把邮件送到收件人手中,在这个比喻中,rabbitmq是一 ...

  4. 《Splunk智能运维实战》——1.7 为本书加载样本数据

    本节书摘来自华章计算机<Splunk智能运维实战>一书中的第1章,第1.7节,作者 [美]乔史·戴昆(Josh Diakun),保罗R.约翰逊(Paul R. Johnson),德莱克·默 ...

  5. Linux 开发之线程条件锁那些事

    2019独角兽企业重金招聘Python工程师标准>>> 条件锁即在一定条件下触发,那什么时候适合用条件锁呢,那当然是你在等待一个符合的条件下触发.一个常用的例子就是在线程中无限循环执 ...

  6. usermod,用户密码管理,mkpasswd命令

    passwd是更改用户密码的文件,如果在root下,我们可以直接输入这个命令更改密码[root@localhost ~]# passwd更改用户 root 的密码 .新的 密码:如果想更改其他用户的密 ...

  7. Jaba_Web--JDBC 查询记录操作模板

    import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import ...

  8. 数学--数论-- HDU 2601 An easy problem(约束和)

    Problem Description When Teddy was a child , he was always thinking about some simple math problems ...

  9. 图论--2-SAT--暴力染色法求字典序最小模版

    #include <cstdio> #include <cstring> #include <stack> #include <queue> #incl ...

  10. P4430 小猴打架、P4981 父子

    prufer编码 当然你也可以理解为 Cayley 公式,其实这个公式就是prufer编码经过一步就能推出的 P4430 小猴打架 P4981 父子 这俩题差不多 先说父子,很显然题目就是让你求\(n ...