昨天,同事优化加锁方式时,出现死锁了

昨天,同事,优化,加锁,方式,出现,死锁 · 浏览次数 : 164

小编点评

# 死锁的预防并发编程 死锁的预防并发编程中,一旦发生了死锁的现象,则基本没有特别好的解决方法,一般情况下只能重启应用来解决。因此,解决死锁的最好方法就是预防死锁。 ### 破坏循环等待条件 破坏循环等待条件,则可以通过对资源排序,按照一定的顺序来申请资源,然后按照顺序来锁定资源,可以有效的避免死锁。例如,在我们的转账操作中,往往每个账户都会有一个唯一的id值,我们在锁定账户资源时,可以按照id值从小到大的顺序来申请账户资源,并按照id从小到大的顺序来锁定账户,此时,程序就不会再进行循环等待了。 程序代码如下所示。 ```java public class TansferAccount{ private Integer id; //账户的id private Integer balance; //账户的余额 //转账操作 public void transfer(TansferAccount target, Integer transferMoney){ while(!requester.applyResources(this, target)){ //循环体为空 ; } try{ //对转出账户加锁 synchronized(beforeAccount){ //对转入账户加锁 synchronized(afterAccount){ if(this.balance >= transferMoney){ this.balance -= transferMoney; target.balance += transferMoney; } } }finally{ //最后释放账户资源 requester.releaseResources(this, target); } } ``` ### 避免死锁 死锁的预防方法就是阻止循环等待条件,将系统中所有的资源设置标志位、排序,规定所有的线程申请资源必须以一定的顺序来操作进而避免死锁。 **例如:** ```java public class TansferAccount{ private Integer id; //账户的id private Integer balance; //账户的余额 //转账操作 public void transfer(TansferAccount target, Integer transferMoney){ if(this.id > target.id){ beforeAccount = target; afterAccount = this; } //对转出账户加锁 synchronized(beforeAccount){ //对转入账户加锁 synchronized(afterAccount){ if(this.balance >= transferMoney){ this.balance -= transferMoney; target.balance += transferMoney; } } } } ``` **总结:** * 死锁的预防并发编程中,一旦发生了死锁的现象,则基本没有特别好的解决方法,一般情况下只能重启应用来解决。 * 避免死锁最简单的方法就是阻止循环等待条件,将系统中所有的资源设置标志位、排序,规定所有的线程申请资源必须以一定的顺序来操作进而避免死锁。

正文

摘要:最近,在优化程序的加锁方式时,竟然出现了死锁!!到底是为什么呢?!经过仔细的分析之后,终于找到了原因。

本文分享自华为云社区《【高并发】优化加锁方式时竟然死锁了!!》,作者: 冰 河。

写在前面

最近,在优化程序的加锁方式时,竟然出现了死锁!!到底是为什么呢?!经过仔细的分析之后,终于找到了原因。

为何需要优化加锁方式?

我们在转账类TansferAccount中使用TansferAccount.class对象对程序加锁,如下所示。

public class TansferAccount{
 private Integer balance;
 public void transfer(TansferAccount target, Integer transferMoney){
 synchronized(TansferAccount.class){
 if(this.balance >= transferMoney){
 this.balance -= transferMoney;
 target.balance += transferMoney;
 } 
 }
 }
}

这种方式确实解决了转账操作的并发问题, 但是这种方式在高并发环境下真的可取吗?试想,如果我们在高并发环境下使用上述代码来处理转账操作,因为TansferAccount.class对象是JVM在加载TansferAccount类的时候创建的,所有的TansferAccount实例对象都会共享一个TansferAccount.class对象。也就是说,所有TansferAccount实例对象执行transfer()方法时,都是互斥的!!换句话说,所有的转账操作都是串行的!!

如果所有的转账操作都是串行执行的话,造成的后果就是:账户A为账户B转账完成后,才能进行账户C为账户D的转账操作。如果全世界的网民一起执行转账操作的话,这些转账操作都串行执行,那么,程序的性能是完全无法接受的!!!

其实,账户A为账户B转账的操作和账户C为账户D转账的操作完全可以并行执行。 所以,我们必须优化加锁方式,提升程序的性能!!

初步优化加锁方式

既然直接TansferAccount.class对程序加锁在高并发环境下不可取,那么,我们到底应该怎么做呢?!

仔细分析下上面的代码业务,上述代码的转账操作中,涉及到转出账户this和转入账户target,所以,我们可以分别对转出账户this和转入账户target加锁,只有两个账户加锁都成功时,才执行转账操作。这样就能够做到账户A为账户B转账的操作和账户C为账户D转账的操作完全可以并行执行。

我们可以将优化后的逻辑用下图表示。

根据上面的分析,我们可以将TansferAccount的代码优化成如下所示。

public class TansferAccount{
 //账户的余额
 private Integer balance;
 //转账操作
 public void transfer(TansferAccount target, Integer transferMoney){
 //对转出账户加锁
 synchronized(this){
 //对转入账户加锁
 synchronized(target){
 if(this.balance >= transferMoney){
 this.balance -= transferMoney;
 target.balance += transferMoney;
 } 
 }
 }
 }
}

此时,上面的代码看上去没啥问题,但真的是这样吗? 我也希望程序是完美的,但是往往却不是我们想的那样啊!没错,上面的程序会出现 死锁, 为什么会出现死锁啊? 接下来,我们就开始分析一波。

死锁的问题分析

TansferAccount类中的代码看上去比较完美,但是优化后的加锁方式竟然会导致死锁!!!这是我亲测得出的结论!!

关于死锁我们可以结合改进的TansferAccount类举一个简单的场景:假设有线程A和线程B两个线程同时运行在两个不同的CPU上,线程A执行账户A向账户B转账的操作,线程B执行账户B向账户A转账的操作。当线程A和线程B执行到 synchronized(this)代码时,线程A获得了账户A的锁,线程B获得了账户B的锁。当执行到synchronized(target)代码时,线程A尝试获得账户B的锁时,发现账户B已经被线程B锁定,此时线程A开始等待线程B释放账户B的锁;而线程B尝试获得账户A的锁时,发现账户A已经被线程A锁定,此时线程B开始等待线程A释放账户A的锁。

这样,线程A持有账户A的锁并等待线程B释放账户B的锁,线程B持有账户B的锁并等待线程A释放账户A的锁,死锁发生了!!

死锁的必要条件

在如何解决死锁之前,我们先来看下发生死锁时有哪些必要的条件。如果要发生死锁,则必须存在以下四个必要条件,四者缺一不可。

互斥条件

在一段时间内某资源仅为一个线程所占有。此时若有其他线程请求该资源,则请求线程只能等待。

不可剥夺条件

线程所获得的资源在未使用完毕之前,不能被其他线程强行夺走,即只能由获得该资源的线程自己来释放(只能是主动释放)。

请求与保持条件

线程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他线程占有,此时请求线程被阻塞,但对自己已获得的资源保持不放。

循环等待条件

既然死锁的发生必须存在上述四个条件,那么,大家是不是就能够想到如何预防死锁了呢?

死锁的预防

并发编程中,一旦发生了死锁的现象,则基本没有特别好的解决方法,一般情况下只能重启应用来解决。因此,解决死锁的最好方法就是预防死锁。

发生死锁时,必然会存在死锁的四个必要条件。也就是说,如果我们在写程序时,只要“破坏”死锁的四个必要条件中的一个,就能够避免死锁的发生。接下来,我们就一起来探讨下如何“破坏”这四个必要条件。

破坏互斥条件

互斥条件是我们没办法破坏的,因为我们使用锁为的就是线程之间的互斥。这一点需要特别注意!!!!

破坏不可剥夺条件

破坏不可剥夺的条件的核心就是让当前线程自己主动释放占有的资源,关于这一点,synchronized是做不到的,我们可以使用java.util.concurrent包下的Lock来解决。此时,我们需要将TansferAccount类的代码修改成类似如下所示。

public class TansferAccount{
 private Lock thisLock = new ReentrantLock();
 private Lock targetLock = new ReentrantLock();
 //账户的余额
 private Integer balance;
 //转账操作
 public void transfer(TansferAccount target, Integer transferMoney){
 boolean isThisLock = thisLock.tryLock();
 if(isThisLock){
 try{
 boolean isTargetLock = targetLock.tryLock();
 if(isTargetLock){
 try{
 if(this.balance >= transferMoney){
 this.balance -= transferMoney;
 target.balance += transferMoney;
 } 
 }finally{
 targetLock.unlock
 }
 }
 }finally{
 thisLock.unlock();
 }
 }
 }
}

其中Lock中有两个tryLock方法,分别如下所示。

  • tryLock()方法

tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。

  • tryLock(long time, TimeUnit unit)方法

tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。

破坏请求与保持条件

破坏请求与保持条件,我们可以一次性申请所需要的所有资源,例如在我们完成转账操作的过程中,我们一次性申请账户A和账户B,两个账户都申请成功后,再执行转账的操作。此时,我们需要再创建一个申请资源的类ResourcesRequester,这个类的作用就是申请资源和释放资源。同时,TansferAccount类中需要持有一个ResourcesRequester类的单例对象,当我们需要执行转账操作时,首先向ResourcesRequester同时申请转出账户和转入账户两个资源,申请成功后,再锁定两个资源;当转账操作完成后,释放锁并释放ResourcesRequester类申请的转出账户和转入账户资源。

ResourcesRequester类的代码如下所示。

public class ResourcesRequester{
 //存放申请资源的集合
 private List<Object> resources = new ArrayList<Object>();
 //一次申请所有的资源
 public synchronized boolean applyResources(Object source, Object target){
 if(resources.contains(source) || resources.contains(target)){
 return false;
 }
 resources.add(source);
 resources.add(targer);
 return true;
 }
 //释放资源
 public synchronized void releaseResources(Object source, Object target){
 resources.remove(source);
 resources.remove(target);
 }
}

此时,TansferAccount类的代码如下所示。

public class TansferAccount{
 //账户的余额
 private Integer balance;
 //ResourcesRequester类的单例对象
 private ResourcesRequester requester;
 //转账操作
 public void transfer(TansferAccount target, Integer transferMoney){
 //自旋申请转出账户和转入账户,直到成功
 while(!requester.applyResources(this, target)){
 //循环体为空
 ;
 }
 try{
 //对转出账户加锁
 synchronized(this){
 //对转入账户加锁
 synchronized(target){
 if(this.balance >= transferMoney){
 this.balance -= transferMoney;
 target.balance += transferMoney;
 } 
 }
 }
 }finally{
 //最后释放账户资源
 requester.releaseResources(this, target);
 }
 }
}

破坏循环等待条件

破坏循环等待条件,则可以通过对资源排序,按照一定的顺序来申请资源,然后按照顺序来锁定资源,可以有效的避免死锁。

例如,在我们的转账操作中,往往每个账户都会有一个唯一的id值,我们在锁定账户资源时,可以按照id值从小到大的顺序来申请账户资源,并按照id从小到大的顺序来锁定账户,此时,程序就不会再进行循环等待了。

程序代码如下所示。

public class TansferAccount{
 //账户的id
 private Integer id;
 //账户的余额
 private Integer balance;
 //转账操作
 public void transfer(TansferAccount target, Integer transferMoney){
 TansferAccount beforeAccount = this;
 TansferAccount afterAccount = target;
 if(this.id > target.id){
 beforeAccount = target;
 afterAccount = this;
 }
 //对转出账户加锁
 synchronized(beforeAccount){
 //对转入账户加锁
 synchronized(afterAccount){
 if(this.balance >= transferMoney){
 this.balance -= transferMoney;
 target.balance += transferMoney;
 } 
 }
 }
 }
}

总结

在并发编程中,使用细粒度锁来锁定多个资源时,要时刻注意死锁的问题。另外,避免死锁最简单的方法就是阻止循环等待条件,将系统中所有的资源设置标志位、排序,规定所有的线程申请资源必须以一定的顺序来操作进而避免死锁。

 

点击关注,第一时间了解华为云新鲜技术~

与昨天,同事优化加锁方式时,出现死锁了相似的内容:

昨天,同事优化加锁方式时,出现死锁了

摘要:最近,在优化程序的加锁方式时,竟然出现了死锁!!到底是为什么呢?!经过仔细的分析之后,终于找到了原因。 本文分享自华为云社区《【高并发】优化加锁方式时竟然死锁了!!》,作者: 冰 河。 写在前面 最近,在优化程序的加锁方式时,竟然出现了死锁!!到底是为什么呢?!经过仔细的分析之后,终于找到了原

MySQL bit类型增加索引后查询结果不正确案例浅析

昨天同事遇到的一个案例,这里简单描述一下:一个表里面有一个bit类型的字段,同事在优化相关SQL的过程中,给这个表的bit类型的字段新增了一个索引,然后测试验证 时,居然发现SQL语句执行结果跟不加索引不一样。加了索引后,SQL语句没有查询出一条记录,删除索引后,SQL语句就能查询出几十条记录。下面

Python学习之五_字符串处理生成查询SQL

Python学习之五_字符串处理生成查询SQL 前言 昨天想给同事讲解一下获取查询部分表核心列信息的SQL方法 也写好了一个简单文档. 但是感觉不是很优雅. 最近两三天晚上一直在学习Python. 想将昨天的文档处理成一个工具的方式. 将查询SQL展示出来. 然后再由同事手工检查确认. 增加时间范围

[转帖]PostgreSQL 参数调整(性能优化)

昨天分别在外网和无外网环境下安装PostgreSQL,有外网环境下安装的相当顺利。但是在无外网环境下就是两个不同的概念了,可谓十有八折。感兴趣的同学可以搭建一下。 PostgreSQL安装完成后第一件事便是做相关测试,然后调整参数。 /*CPU 查看CPU型号*/ cat /proc/cpuinfo

Python学习之二:不同数据库相同表是否相同的比较方法

摘要 昨天学习了使用python进行数据库主键异常的查看. 当时想我们有跨数据库的数据同步场景. 对应的我可以对不同数据库的相同表的核心字段进行对比. 这样的话能够极大的提高工作效率. 我之前写过很长时间的shell.昨天跟着同事开始学python. 感觉的确用python能够节约大量的时间. 生活

TiDB恢复部分表的方式方法

TiDB恢复部分表的方式方法 背景 今天同事告知误删了部分表. 因为是UAT准生产的环境, 所以仅有每天晚上11点的备份处理. 同时告知 昨天的数据也可以. 得到认可后进行了 TiDB的单表备份恢复. 备份的语句 注意TiDB是可以增量备份恢复的 但是为了快速的恢复和解决背景中的问题. 我这边采用保

Chrony 的学习与使用

Chrony 的学习与使用 背景 之前捯饬 ntp 发现很麻烦, 经常容易弄错了. 昨天处理文件精确时间时 想到了时间同步. 发现只有自己总结的ntpdate 但是还没有 chronyd相关的总结 本着自己写笔记是为了快速解决问题的思路 想趁着孩子上课, 将快速解决方案部署一下. 安装 yum in

发现XWPFDocument写入Word文档时的小BUG:两天的探索与解决之旅

引言 最近在使用XWPFDocument生成Word文档时,遇到一个错误:“未将对象引用设置到对象的实例”。这个平常很容易找到原因的问题却困扰了我两天,最终发现问题出在设置段落时赋值了空值。本文将详细记录这个问题的原因及解决方法,希望能对遇到相同问题的开发者有所帮助。 第一天:问题的发现 事情的开始

将IoTDB注册为Windows服务

昨天写的文章《Windows Server上部署IoTDB集群》,Windows下的IoTDB是控制台程序,打开窗口后,很容易被别人给关掉,因此考虑做成Windows服务,nssm正是解决该问题的利器。 1.下载nssm:http://www.nssm.cc/download 查看官网提示,如果是w

讯飞星火大模型 与New Bing实测对比

昨天科大讯飞发布了讯飞星火认知大模型,在发布会现场实测大模型的7种核心能力,并发布了它在教育、办公、汽车、数字员工领域的应用成果。科大讯飞董事长刘庆峰表示:认知大模型展示了通用人工智能的曙光,讯飞星火认知大模型已在文本生成、知识问答、数学能力3种能力上超越ChatGPT。NewBing 也全面开放给