银行转账问题(死锁)

news2024/11/17 21:51:44

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

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);
    }
    
    @Override
    public 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()+"获取到第一把锁");
            //加入线程睡眠500ms
            try {
                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{
            @Override
            public 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());
            }
        }
    }

    @Override
    public 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和b
        Thread 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);
    }

    @Override
    public void run() {
        if (flag == 1) {
            //a转账给b
            transferMoney(a, b, 200);
        }
        if (flag == 0) {
            //b转账给a
            transferMoney(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

谢谢关注!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/604666.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

我记不住的那些C语言的struct知识

背景&#xff1a; 最近在重学C语言&#xff0c;目的是为了能看懂操作系统的底层代码&#xff0c;也为后续使用C语言开发一个类似redis数据库的中间件做准备&#xff0c;于是又重新踏上了学习C语言的道路&#xff0c;早在上学期间就学习过C语言&#xff0c;但是很久都不用了&…

ssm学习-spring01

Spring_day01 今日目标 掌握Spring相关概念完成IOC/DI的入门案例编写掌握IOC的相关配置与使用掌握DI的相关配置与使用1,课程介绍 对于一门新技术,我们需要从为什么要学、学什么以及怎么学这三个方向入手来学习。那对于Spring来说: 1.1 为什么要学? 从使用和占有率看 Spri…

使用 ChatGPT API 构建系统(一):分类

今天我学习了DeepLearning.AI的 Building Systems with the ChatGPT API 的在线课程&#xff0c;我想和大家一起分享一下该门课程的一些主要内容。 下面是我们通过Openai API来访问ChatGPT模型的主要代码&#xff1a; import openai#您的openai的api key openai.api_key YOUR…

chatgpt赋能python:Python删除节点:从入门到实践

Python删除节点&#xff1a;从入门到实践 在任何编程语言中&#xff0c;删除节点都是一个极为常见的操作。在Python中&#xff0c;它同样非常重要&#xff0c;因为我们通常会使用Python处理各种数据结构&#xff0c;诸如树、链表等等。但是&#xff0c;删除节点并不总是一件容…

C++类和对象 -- 知识点补充

补充 const成员函数static成员友元内部类匿名对象拷贝对象时的一些编译器优化 const成员函数 将const修饰的成员函数称为const成员函数&#xff0c;const修饰类成员函数&#xff0c;实际是修饰该成员函数隐含的this指针&#xff0c;表明在该成员函数中不能对类的成员进行修改。…

javaWeb ssh自习室管理系统myeclipse开发mysql数据库MVC模式java编程计算机网页设计

一、源码特点 java ssh自习室管理系统是一套完善的web设计系统&#xff08;系统采用ssh框架进行设计开发&#xff09;&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S 模式开发。开发环境为TOMCAT7.0,…

预报名通道已开启,2023第11届国际生物发酵展,8月4-6日上海见!

新机遇、新挑战、新发展 同期展会&#xff1a;酵素产品与益生产品展 制药机械与包装技术展 生化仪器及实验室设备展 合成生物技术与生物制造展 展会时间&#xff1a; 2023年8月4日 9:00-17:00 2023年8月5日 9:00-17:00 2023年8月6日 9:00-15:00 展会地点&#xff1a…

【数据结构】栈和队列选择题和面试编程题

目录 一、选择题 二、栈和队列的面试题 1、括号匹配问题 1.1 题目说明 1.2 题目解析 2、用队列实现栈 2.1 题目说明 2.2 题目解析 3、用栈实现队列 3.1 题目说明 3.2 题目解析 一、选择题 1、若进栈序列为 1,2,3,4 &#xff0c;进栈过程中可以出栈&#xff0c;则下列不可能的…

软考A计划-电子商务设计师-信息安全知识

点击跳转专栏>Unity3D特效百例点击跳转专栏>案例项目实战源码点击跳转专栏>游戏脚本-辅助自动化点击跳转专栏>Android控件全解手册点击跳转专栏>Scratch编程案例 &#x1f449;关于作者 专注于Android/Unity和各种游戏开发技巧&#xff0c;以及各种资源分享&am…

安全防御——IDS(入侵检测系统)

安全防御——IDS&#xff08;入侵检测系统&#xff09; IDS介绍为什么需要IDSIDS的工作原理IDS的工作过程第一步&#xff1a;信息收集第二步&#xff1a;数据分析 IDS的主要检测方法1、模式匹配&#xff08;误用检测&#xff09;2、统计分析&#xff08;异常检测&#xff09;3、…

chatgpt赋能python:Python创建venv的完全指南

Python创建venv的完全指南 在Python开发中&#xff0c;虚拟环境是一个非常有用的工具。它可以让我们在同一台计算机上拥有多个Python环境&#xff0c;而不会互相干扰。在本文中&#xff0c;我们将介绍如何使用Python创建venv&#xff08;虚拟环境&#xff09;。 什么是venv&a…

4-5.配置信息和路由信息

一、配置信息 app.run()的参数 参数1&#xff1a;host&#xff0c;如果我们不指定&#xff0c;默认值是127.0.0.1。参数2&#xff1a;port&#xff0c;如果我们不指定&#xff0c;默认值是5000。参数3&#xff1a;debug&#xff0c;调试模式&#xff0c;如果不指定&#xff0…

chatgpt赋能python:Python创建画布语句

Python 创建画布语句 在数据可视化的领域&#xff0c;画布&#xff08;Canvas&#xff09;是一个重要的概念。画布可以视为一个空白的像素或向量画布&#xff0c;用于绘制图表、图形、图像和动画等。Python 提供了多种创建画布的方式&#xff0c;其中包括使用第三方库和内置库…

哲学家就餐问题(死锁)

本文主要讲述死锁的一个经典案例—哲学家就餐问题&#xff0c;并对该问题进行修复。 1. 问题描述 看上图&#xff0c;有五位哲学家&#xff0c;每天循环做两件事&#xff1a;思考&#xff0c;吃面。吃面时每人面前都有一个盘子&#xff0c;盘子左边和右边都有一根筷子&#xff…

5.3 树和二叉树的抽象数据类型定义

博主简介&#xff1a;一个爱打游戏的计算机专业学生博主主页&#xff1a; 夏驰和徐策所属专栏&#xff1a;算法设计与分析 1.什么是树的抽象数据类型定义 树的抽象数据类型定义是指对树这种数据结构的一种抽象描述&#xff0c;其中包括了树的基本操作和性质。它定义了树作为一…

基于深度学习的目标姿态检测方法_kaic

目录 摘要 第1章 引言 1.1 研究背景和意义 1.2 国内外研究现状 1.3 主要内容 第2章 单目相机的目标姿态检测技术 2.1单目相机的工作原理 2.2目标姿态检测 2.3已有的目标姿态检测方法及其局限性 2.4本章总结 第3章 构建数据集 3.1 数据集来源 3.2数据集标注 3.3数据集分析 3.4本…

基于Springboot的社区论坛系统(源代码+数据库)055

部分代码地址 https://gitee.com/ynwynwyn/forum-public 基于Springboot的社区论坛系统(源代码数据库) 一、系统介绍 前台&#xff1a; 话题列表&#xff0c;搜索话题&#xff0c;发布话题通过标签筛选话题个人设置&#xff1a;修改个人信息&#xff0c;查看发布话题记录&a…

FPGA设计的指导性原则 (三)

例12. 在SDC文件中附加syn_ramstyle综合约束属性,指定综合存贮单元的类型 SDC文件是Synplicity综合工具通用的综合约束属性文件,其扩展名为”sde”。在SDC 指定 syn_ramstyle的语法格式为: define_attribute (signal_name [bit_range)) syn_ramstyle (atring) 其中,黑体…

1.Python高频函数—数据合并merge()

前言 数据处理中经常对多个表的数据进行合并处理&#xff0c;python 提供两个十分好用的函数处理。merge() 、 concat() merger函数是Python里的数据分析工作中最常见的函数之一&#xff0c;主要应用场景是&#xff1a;针对同一个主键存在两张不同字段的表。&#xff08;这里强…

《Kali渗透基础》05. 主动信息收集(二)

kali渗透 1&#xff1a;端口扫描2&#xff1a;UDP 扫描2.1&#xff1a;Scapy2.2&#xff1a;nmap 3&#xff1a;半开放扫描3.1&#xff1a;Scapy3.2&#xff1a;nmap3.3&#xff1a;hping3 4&#xff1a;全连接扫描4.1&#xff1a;Scapy4.2&#xff1a;nmap4.3&#xff1a;dmit…