本文主要讲述死锁的一个经典案例—银行转账问题,并对该问题进行定位、修复。

1. 问题说明

当账户A对账户B进行转账时,

  • 首先需要获取到两把锁:账户A和账户B的锁。
  • 获取两把锁成功,且余额大于0,则扣除转出人的余额,并增加收款人的余额,而且这些操作都是在一个原子操作
  • 获取锁的顺序相反导致死锁,即线程1获取到账户A的锁,然后请求账户B的锁,线程2已经获取到账户B的锁,然后请求A的锁,结果两者互相等待对方的锁,造成死锁。

2. 代码演示

public class TransferMoney implements Runnable {Integer flag = 1;static Account a = new Account(1000);static Account b = new Account(1000);//主函数public static void main(String[] args) throws InterruptedException {TransferMoney t1 = new TransferMoney();TransferMoney t2 = new TransferMoney();t1.flag = 1;t1.flag = 0;Thread thread1 = new Thread(t1);Thread thread2 = new Thread(t2);thread1.start();thread2.start();thread1.join();thread1.join();System.out.println("a的余额为:"+a.balance);System.out.println("a的余额为:"+b.balance);}@Overridepublic void run() {if (flag == 1) {transferMoney(a, b, 500);}if (flag == 0) {transferMoney(b, a, 500);}}// 转账private void transferMoney(Account from, Account to, int amount) {synchronized (from) {System.out.println(Thread.currentThread().getName()+"获取到第一把锁");//加入线程睡眠500mstry {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}synchronized (to) {System.out.println(Thread.currentThread().getName()+"获取到第二把锁");if (from.balance - amount < 0) {System.out.println("余额不足,转账失败。");return;}from.balance -= amount;to.balance += amount;System.out.println("成功转账" + i + "元");}}}static class Account {//余额int balance;public Account(int balance) {this.balance = balance;}}
}

打印结果:

启动程序,发现打印结果输出几句之后就不再输出,而且程序也未停止,这就发生了死锁。

  • 当线程 thread1 执行transferMoney()方法的时候,他拿到from锁,也就是里面的类成员变量a;
  • 经过 500ms,这个期间线程thread2进来执行transferMoney()方法,拿到from锁,也就是类成员变量b
  • 接下来500ms之后线程thread1继续执行,但是他要拿到to锁,也就是他的成员变量b,但是已经被线程thread1拿过去作为他的from锁了
  • 线程thread2接下来拿他的to锁,也就是成员变量a,但是他已经被线程thread1拿着了,因为成员变量a是线程1的from锁;

所以就进入了死锁的情况。

下面模拟多人转账,多个线程陷入死锁:

public class MultiTransferMoney {private static final int NUM_ACCOUNTS = 5000;//账号数private static final int NUM_MONEY = 1000;//余额private static final int NUM_ITERATIONS = 10000000;//转账次数private static final int NUM_THREADS = 20;//转账人数public static void main(String[] args) {Random random = new Random();TransferMoney.Account[] accounts = new TransferMoney.Account[NUM_ACCOUNTS];//初始化for (int i = 0; i < accounts.length; i++) {accounts[i] = new TransferMoney.Account(NUM_MONEY);}//转账类class TransferThread extends Thread{@Overridepublic void run() {for (int i = 0; i < NUM_ITERATIONS; i++) {//随机下标int fromAcct = random.nextInt(NUM_ACCOUNTS);int toAcct = random.nextInt(NUM_ACCOUNTS);int amount = random.nextInt(NUM_ACCOUNTS);transferMoney(accounts[fromAcct], accounts[toAcct],amount);}System.out.println("程序结束!!!!");}private void transferMoney(Account from, Account to, int amount) {synchronized (from) {synchronized (to) {if (from.balance - amount < 0) {System.out.println("余额不足,转账失败。");return;}from.balance -= amount;to.balance += amount;System.out.println("成功转账" + i + "元");}}}}//线程数for (int i = 0; i < NUM_THREADS; i++) {new TransferThread().start();}}
}

打印结果:

在多人同时转账的情况下,虽然很人数很多,发生死锁的概率变小,但是只要发生死锁的风险存在,随着时间的推移,就一定会导致程序陷入死锁(墨菲定律)。

3. 如何定位死锁的位置(以两人转账的代码为例)

(1)jstack 命令

通过使用java自带的jstack命令,来查找我们项目中的死锁问题

## 需要首先获取程序的进程 pid
jps
## 然后在 终端界面执行如下命令
jstack 8359  #javahome下的jastack命令 进程的pid

执行结果图:

可以清晰地看到 Thread-1 拿到了锁 <0x000000076adae688> ,正在等待 <0x000000076adae678>,而 Thread-0 拿到了锁 <0x000000076adae678>,正在等待 <0x000000076adae688>,于是两者互相等待,造成死锁。

(2)ThreadMXBean代码

/***      通过 ThreadMXBean 检测死锁*/
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;public class ThreadMXBeanDetection implements Runnable{int flag = 1;//标记位static Object lock1 = new Object();static Object lock2 = new Object();public static void main(String[] args) throws InterruptedException {ThreadMXBeanDetection r1 = new ThreadMXBeanDetection();ThreadMXBeanDetection r2 = new ThreadMXBeanDetection();r1.flag=1;r2.flag=0;Thread thread1 = new Thread(r1);Thread thread2 = new Thread(r2);thread1.start();thread2.start();Thread.sleep(1000);//得到实例ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();//发现死锁if (deadlockedThreads != null && deadlockedThreads.length>0){//迭代for (long item : deadlockedThreads) {//获取线程信息ThreadInfo threadInfo = threadMXBean.getThreadInfo(item);//获取死锁线程的名字System.out.println("发现死锁:"+threadInfo.getThreadName());}}}@Overridepublic void run() {System.out.println("flag= " + flag);if (flag == 1) {synchronized (lock1){try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}synchronized (lock2){System.out.println(flag);}}}if (flag == 0) {synchronized (lock2){try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}synchronized (lock1){System.out.println(flag);}}}}
}

打印结果:

如上图所示,ThreadMXBean 可以检测死锁,如果我们检测到了之后,就可以编写对应的逻辑,比如重启线程、通知告警系统、发消息提醒运维人员等。

4. 死锁修复(以两人转账的代码为例)

本公众号的《死锁细究》这篇文章提到多种死锁修复的方案。

本文使用死锁避免策略:把获取两把锁的规则改一下,原来的规则是先获取转出人的锁,再获取收款人的锁,这就会造成两个转出人都在等对方释放锁的情况。

现在我们把规则改成:所有的交易都先获取hash值更小的锁,获取到了hash小的锁才能获取hash大的锁,这就避免了环形的死锁,假如说这两个锁的大小一样,这时候就需要一把额外的锁来进行交易流程的控制,相当于一场“加时赛”。

在实际业务开发中可以使用主键,因为主键是唯一的,可以用主键来决定获取锁的顺序。

代码展示:

public class TransferMoney implements Runnable {private int flag = 1;private static Account a = new Account(500);private static Account b = new Account(500);private static Object lock = new Object();public static void main(String[] args) throws InterruptedException {TransferMoney r1 = new TransferMoney();TransferMoney r2 = new TransferMoney();r1.flag = 1;r2.flag = 0;//定义两个线程,flag = 1和0分别模拟a和bThread t1 = new Thread(r1);Thread t2 = new Thread(r2);t1.start();t2.start();t1.join();t2.join();System.out.println("a的余额" + a.balance);System.out.println("b的余额" + b.balance);}@Overridepublic void run() {if (flag == 1) {//a转账给btransferMoney(a, b, 200);}if (flag == 0) {//b转账给atransferMoney(b, a, 200);}}/*** 转账方法** @param from   转出人* @param to     收款人* @param amount 转账金额*/public static void transferMoney(Account from, Account to, int amount) {/*** 辅助类*/class Helper {public void transfer() {if (from.balance - amount < 0) {System.out.println("余额不足,转账失败。");return;}from.balance -= amount;to.balance = to.balance + amount;System.out.println("成功转账" + amount + "元");}}//使用类的hash值来帮助排序int fromHash = System.identityHashCode(from);int toHash = System.identityHashCode(to);// 通过System.identityHashCode(XXX)去获取对象的hash值,并进行比较他们的hash值来进行比较来决定拿锁的顺序if (fromHash < toHash) {synchronized (from) {System.out.println(Thread.currentThread().getName() + "获取到第一把锁");synchronized (to) {System.out.println(Thread.currentThread().getName() + "获取到第二把锁");new Helper().transfer();}}} else if (fromHash > toHash) {synchronized (to) {System.out.println(Thread.currentThread().getName() + "获取到第一把锁");synchronized (from) {System.out.println(Thread.currentThread().getName() + "获取到第二把锁");new Helper().transfer();}}} else {//发生hash碰撞时,可以增加第三把锁来进行控制,类似“加时赛”synchronized (lock) {synchronized (to) {System.out.println(Thread.currentThread().getName() + "获取到第一把锁");synchronized (from) {System.out.println(Thread.currentThread().getName() + "获取到第二把锁");new Helper().transfer();}}}}}/*** 收款账户*/static class Account {public Account(int balance) {this.balance = balance;}int balance;}
}

打印结果:

程序不再死锁!!!

文章来源:银行转账问题(死锁)

个人微信:CaiBaoDeCai

微信公众号名称:Java知者

微信公众号 ID: JavaZhiZhe

谢谢关注!

银行转账问题(死锁)相关推荐

  1. Java并发学习三:银行转账的死锁问题解决及示例

    Java并发学习系列文章:Java并发学习-博客专栏 今天在学习极客时间专栏:<Java并发编程实战> 从03 | 互斥锁(上):解决原子性问题到06 | 用"等待-通知&quo ...

  2. 剑问天下java_Java并发编程精讲

    第1章 开宗明义[不看错过一个亿] 1-1 课程综述--特点和内容介绍 (16:07) 第2章 跨越第一座山"线程八大核心"[适用于纵观全貌] 2-1 纵观全貌--线程八大核心 ( ...

  3. 银行转账带来的死锁问题

    如果只是单纯的进行两个账户之间的转账,那么久很容易导致死锁问题 std::mutex mxA; std::mutex mxB; int countA = 1000; int countB = 1000 ...

  4. java产生死锁的主要原因_详解java中产生死锁的原因及如何避免

    1. Java中导致死锁的原因 Java中死锁最简单的情况是,一个线程T1持有锁L1并且申请获得锁L2,而另一个线程T2持有锁L2并且申请获得锁L1,因为默认的锁申请操作都是阻塞的,所以线程T1和T2 ...

  5. Java多线程中的死锁问题

    Java程序基本都要涉及到多线程,而在多线程环境中不可避免的要遇到线程死锁的问题.Java不像数据库那么能够检测到死锁,然后进行处理,Java中的死锁问题,只能通过程序员自己写代码时避免引入死锁的可能 ...

  6. java中产生死锁的原因及如何避免

    转载自 https://blog.csdn.net/m0_38126177/article/details/78587845 1. Java中导致死锁的原因 Java中死锁最简单的情况是,一个线程T1 ...

  7. java避免活锁.死锁的解决,java并发编程(九): 避免活跃性危险

    避免活跃性危险: 本部分讨论活跃性故障的原因,及如何避免它们. 死锁: 典型的哲学家进餐问题. 锁顺序死锁: 如上面哲学家进餐有可能发生下面的情况: 上面发生死锁的根本原因在于两个线程以不同的顺序来获 ...

  8. 出现死锁的场景分析及解决方法

    在上一篇互斥锁的时候最后使用Account.class作为互斥锁,来解决转载问题,所有的账户转账操作都是串行的,性能太差. 我们可以考虑缩小锁定的范围,使用细粒度的锁,来提高并行度.例如用两把锁,转出 ...

  9. java如何防止死锁_Java 死锁以及如何避免?

    Java中产生死锁的原因以及如何避免: 1. Java中导致死锁的原因 Java中死锁最简单的情况是,一个线程T1持有锁L1并且申请获得锁L2,而另一个线程T2持有锁L2并且申请获得锁L1,因为默认的 ...

最新文章

  1. c语言 爬虫 socket,爬虫遇到 Socket,莫慌,肝就完了!
  2. 五条强化 SSH 安全的建议
  3. java B2B2C Springcloud多租户电子商城系统-spring-cloud-eureka
  4. 【Java】5.1 类和对象
  5. scp有证书的传输,压缩,解压
  6. 疫情之下的科技普惠:阿里云科技驱动中小企业数字化
  7. 注意职场“十不要”,让你少奋斗30年
  8. MySQL 常用30种SQL查询语句优化方法
  9. python基础知识-8-三元和一行代码(推导式)
  10. 小白学JAVA,与你们感同身受,JAVA---day6:抽象类接口的理解。鲁迅的一句话:总之岁月漫长,然而值得等待。
  11. Web笔记-html中svg的基本使用
  12. 终于找到原因!大厂面试被拒,是你不会数据结构
  13. java xss 参数_JAVA 重写HttpServletRequest的获取参数防止xss攻击
  14. python基础(1)——简介与安装
  15. 1.两数之和(力扣leetcode) 博主可答疑该问题
  16. bch码原理基于matlab,BCH码编译码matlab仿真
  17. 高斯函数及高斯滤波器
  18. C# NotifyIcon(通知区图标动画)
  19. Mugeda(木疙瘩)H5案例课—教你玩转密室逃脱类H5-岑远科-专题视频课程
  20. VOIP系统开源实现PCMA/PCMU/AMR/G729常用格式解码服务

热门文章

  1. MediaCodec_Analyze-2-config
  2. mysql ansi nulls_SQL学习笔记之ANSI_NULLS
  3. spark基础transformation
  4. linux运行wordcount,hadoop运行第一个实例wordcount
  5. 使用python管理百度云存储
  6. 排查链接是否失效_锅炉主保护系统隐患排查与治理
  7. Android设计模式详解之解释器模式
  8. 详解HMM模型 及 实现(之一:problem1)
  9. 回转寿司——有趣的分块
  10. 百度地图 地理位置 转 经纬度 计算两点间的距离