Java并发编程实战 04死锁了怎么办?
Java并发编程文章系列
Java并发编程实战 01并发编程的Bug源头
Java并发编程实战 02Java如何解决可见性和有序性问题
Java并发编程实战 03互斥锁 解决原子性问题
前提
在第三篇文章最后的例子当中,需要获取到两个账户的锁后进行转账操作,这种情况有可能会发生死锁,我把上一章的代码片段放到下面:
public class Account {
// 余额
private Long money;
public synchronized void transfer(Account target, Long money) {
synchronized(this) { (1)
synchronized (target) { (2)
this.money -= money;
if (this.money < 0) {
// throw exception
}
target.money += money;
}
}
}
}
若账户A转账给账户B100元,账户B同时也转账给账户A100元,当账户A转帐的线程A执行到了代码(1)处时,获取到了账户A对象的锁,同时账户B转账的线程B也执行到了代码(1)处时,获取到了账户B对象的锁。当线程A和线程B执行到了代码(2)处时,他们都在互相等待对方释放锁来获取,可是synchronized是阻塞锁,没有执行完代码块是不会释放锁的,就这样,线程A和线程B死死的对着,谁也不放过谁。等到了你去重启应用的那一天。。。这个现象就是死锁。
死锁的定义:一组互相竞争资源的线程因互相等待,导致“永久”阻塞的现象。
如下图:

查找死锁信息
这里我先以一个基本会发生死锁的程序为例,创建两个线程,线程A获取到锁A后,休眠1秒后去获取锁B;线程B获取到锁B后 ,休眠1秒后去获取锁A。那么这样基本都会发生死锁的现象,代码如下:
public class DeadLock extends Thread {
private String first;
private String second;
public DeadLock(String name, String first, String second) {
super(name); // 线程名
this.first = first;
this.second = second;
}
public void run() {
synchronized (first) {
System.out.println(this.getName() + " 获取到锁: " + first);
try {
Thread.sleep(1000L); //线程休眠1秒
synchronized (second) {
System.out.println(this.getName() + " 获取到锁: " + second);
}
} catch (InterruptedException e) {
// Do nothing
}
}
}
public static void main(String[] args) throws InterruptedException {
String lockA = "lockA";
String lockB = "lockB";
DeadLock threadA = new DeadLock("ThreadA", lockA, lockB);
DeadLock threadB = new DeadLock("ThreadB", lockB, lockA);
threadA.start();
threadB.start();
threadA.join(); //等待线程1执行完
threadB.join();
}
}
运行程序后将发生死锁,然后使用jps命令(jps.exe在jdk/bin目录下),命令如下:
C:\Program Files\Java\jdk1.8.0_221\bin>jps -l
24416 sun.tools.jps.Jps
24480 org.jetbrains.kotlin.daemon.KotlinCompileDaemon
1624
20360 org.jetbrains.jps.cmdline.Launcher
9256
9320 page2.DeadLock
18188
可以发现发生死锁的进程id 9320,然后使用jstack(jstack.exe在jdk/bin目录下)命令查看死锁信息。
C:\Program Files\Java\jdk1.8.0_221\bin>jstack 9320
"ThreadB" #13 prio=5 os_prio=0 tid=0x000000001e48c800 nid=0x51f8 waiting for monitor entry [0x000000001f38f000]
java.lang.Thread.State: BLOCKED (on object monitor)
at page2.DeadLock.run(DeadLock.java:19)
- waiting to lock <0x000000076b99c198> (a java.lang.String)
- locked <0x000000076b99c1d0> (a java.lang.String)
"ThreadA" #12 prio=5 os_prio=0 tid=0x000000001e48c000 nid=0x3358 waiting for monitor entry [0x000000001f28f000]
java.lang.Thread.State: BLOCKED (on object monitor)
at page2.DeadLock.run(DeadLock.java:19)
- waiting to lock <0x000000076b99c1d0> (a java.lang.String)
- locked <0x000000076b99c198> (a java.lang.String)
这样我们就可以看到发生死锁的信息。虽然发现了死锁,但是解决死锁只能是重启应用了。
如何避免死锁的发生
1.固定的顺序来获得锁
如果所有线程以固定的顺序来获得锁,那么在程序中就不会出现锁顺序死锁问题。(取自《Java并发编程实战》一书)
要想验证锁顺序的一致性,有很多种方式,如果锁定的对象含有递增的id字段(唯一、不可变、具有可比性的),那么就好办多了,获取锁的顺序以id由小到大来排序。还是用转账的例子来解释,代码如下:
public class Account {
// id (递增)
private Integer id;
// 余额
private Long money;
public synchronized void transfer(Account target, Long money) {
Account account1;
Account account2;
if (this.id < target.id) {
account1 = this;
account2 = target;
} else {
account1 = target;
account2 = this;
}
synchronized(account1) {
synchronized (account2) {
this.money -= money;
if (this.money < 0) {
// throw exception
}
target.money += money;
}
}
}
}
若该对象并没有唯一、不可变、具有可比性的的字段(如:递增的id),那么可以使用 System.identityHashCode() 方法返回的哈希值来进行比较。比较方式可以和上面的例子一类似。System.identityHashCode()虽然会出现散列冲突,但是发生冲突的概率是非常低的。因此这项技术以最小的代价,换来了最大的安全性。
提示: 不管你是否重写了对象的hashCode方法,System.identityHashCode() 方法都只会返回默认的哈希值。
2.一次性申请所有资源
只要同时获取到转出账户和转入账户的资源锁。执行完转账操作后,也同时释放转入账户和转出账户的资源锁。那么则不会出现死锁。但是使用synchronized只能同时锁定一个资源锁,所以需要建立一个锁分配器LockAllocator 。代码如下:
/** 锁分配器(单例类) */
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);
}
}
}
使用while循环不断的去获取锁,一直到获取成功,当然你也可以设置获取失败后休眠xx毫秒后获取,或者其他优化的方式。释放锁必须使用try-finally的方式来释放锁。避免释放锁失败。
3.尝试获取锁资源
在Java中,Lock接口定义了一组抽象的加锁操作。与内置锁synchronized不同,使用内置锁时,只要没有获取到锁,就会死等下去,而显示锁Lock提供了一种无条件的、可轮询的、定时的以及可中断的锁获取操作,所有加锁和解锁操作都是显示的(内置锁synchronized的加锁和解锁操作都是隐示的),这篇文章就不展开来讲显示锁Lock了(当然感兴趣的朋友可以先百度一下)。
总结
在生产环境发生死锁可是一个很严重的问题,虽说重启应用来解决死锁,但是毕竟是生产环境,代价很大,而且重启应用后还是可能会发生死锁,所以在编写并发程序时需要非常严谨的避免死锁的发生。避免死锁的方案应该还有更多,鄙人不才,暂知这些方案。若有其它方案可以留言告知。非常感谢你的阅读,谢谢。
参考文章:
《Java并发编程实战》第10章
极客时间:Java并发编程实战 05:一不小心死锁了,怎么办?
极客时间:Java核心技术面试精讲 18:什么情况下Java程序会产生死锁?如何定位、修复?
个人博客网址: https://colablog.cn/
如果我的文章帮助到您,可以关注我的微信公众号,第一时间分享文章给您

Java并发编程实战 04死锁了怎么办?的更多相关文章
- Java并发编程实战 05等待-通知机制和活跃性问题
Java并发编程系列 Java并发编程实战 01并发编程的Bug源头 Java并发编程实战 02Java如何解决可见性和有序性问题 Java并发编程实战 03互斥锁 解决原子性问题 Java并发编程实 ...
- Java并发编程实战 03互斥锁 解决原子性问题
文章系列 Java并发编程实战 01并发编程的Bug源头 Java并发编程实战 02Java如何解决可见性和有序性问题 摘要 在上一篇文章02Java如何解决可见性和有序性问题当中,我们解决了可见性和 ...
- 【Java并发编程实战】----- AQS(三):阻塞、唤醒:LockSupport
在上篇博客([Java并发编程实战]----- AQS(二):获取锁.释放锁)中提到,当一个线程加入到CLH队列中时,如果不是头节点是需要判断该节点是否需要挂起:在释放锁后,需要唤醒该线程的继任节点 ...
- 【Java并发编程实战】-----“J.U.C”:ReentrantReadWriteLock
ReentrantLock实现了标准的互斥操作,也就是说在某一时刻只有有一个线程持有锁.ReentrantLock采用这种独占的保守锁直接,在一定程度上减低了吞吐量.在这种情况下任何的"读/ ...
- 【Java并发编程实战】-----“J.U.C”:ReentrantLock之三unlock方法分析
前篇博客LZ已经分析了ReentrantLock的lock()实现过程,我们了解到lock实现机制有公平锁和非公平锁,两者的主要区别在于公平锁要按照CLH队列等待获取锁,而非公平锁无视CLH队列直接获 ...
- 【Java并发编程实战】-----“J.U.C”:ReentrantLock之一简介
注:由于要介绍ReentrantLock的东西太多了,免得各位客官看累,所以分三篇博客来阐述.本篇博客介绍ReentrantLock基本内容,后两篇博客从源码级别分别阐述ReentrantLock的l ...
- Java并发编程实战.笔记十一(非阻塞同步机制)
关于非阻塞算法CAS. 比较并交换CAS:CAS包含了3个操作数---需要读写的内存位置V,进行比较的值A和拟写入的新值B.当且仅当V的值等于A时,CAS才会通过原子的方式用新值B来更新V的值,否则不 ...
- Java并发编程实战——读后感
未完待续. 阅读帮助 本文运用<如何阅读一本书>的学习方法进行学习. P15 表示对于书的第15页. Java并发编程实战简称为并发书或者该书之类的. 熟能生巧,不断地去理解,就像欣赏一部 ...
- 【Java并发编程实战】----- AQS(四):CLH同步队列
在[Java并发编程实战]-–"J.U.C":CLH队列锁提过,AQS里面的CLH队列是CLH同步锁的一种变形.其主要从两方面进行了改造:节点的结构与节点等待机制.在结构上引入了头 ...
随机推荐
- Python 3.9 性能优化:更快的 list()、dict() 和 range() 等内置类型
Python 的 3.9.0 版本正在开发中,计划在 2020-10-05 发布 final 版本. 官方在 changelog 中披露了很多细节,其中有一项"vectorcall" ...
- 使用NLP从文章中自动提取关键字
背景 在研究和新闻文章中,关键词构成了一个重要的组成部分,因为它们提供了文章内容的简洁表示.关键词在从信息检索系统,书目数据库和搜索引擎优化中定位文章方面也起着至关重要的作用.关键词还有助于将文章分类 ...
- pytorch torchversion标准化数据
新旧标准差的关系
- Docker 搭建 ELK 集群步骤
前言 本篇文章主要介绍在两台机器上使用 Docker 搭建 ELK. 正文 环境 CentOS 7.7 系统 Docker version 19.03.8 docker-compose version ...
- 报错:require_once cannot allocate memory----php,以前自己弄的稍微有点特殊的开发环境
最近出现过一个问题,值得记录 类似于这样的报错的问题: Warning: require_once(/www/app/somecomponent.php): failed to open stream ...
- Spring Boot filter
在Spring Boot中自定义filter 本文我们将会讲解如何在Spring Boot中自定义filter并指定执行顺序. 定义Filter很简单,我们只需要实现Filter接口即可,同时我们可指 ...
- CSS开发技巧(四):解决flex多行布局的行间距异常、子元素高度拉伸问题
在使用flex布局时,若出现换行,有两种较为特殊的现象是值得我们研究的: 子元素高度被拉伸,其实际高度大于它的内容高度. 各行子元素之间的行间距过大,甚至我们根本没有给子元素设置margin. 现在我 ...
- 写给Java程序员的Java虚拟机学习指南
大家好,我是极客时间<深入拆解Java虚拟机>作者.Oracle Labs高级研究员郑雨迪.有幸借这个专题的机会,能和大家分享为何Java工程师要学Java虚拟机?如何掌握Java虚拟机? ...
- solr7.4创建core,导入MySQL数据,中文分词
#solr版本:7.4.0 一.新建Core 进入安装目录下得server/solr/,创建一个文件夹,如:new_core 拷贝server/solr/configsets/_default/con ...
- 通用权限管理系统组件 (GPM - General Permissions Manager)
有的公司开发人员只那么几个,一个人支撑整个公司所有的IT系统实在有点累,不想自己写权限系统了,自己琢磨不也是要花时间和精力,要为此付出多少时间和汗水阿,细细的皱纹不知要多多少呢,重复建设不是白白浪费生 ...