【java_多线程】模拟银行转账,为保证原子性而引发死锁,检测死锁并解决

41 篇文章 0 订阅
5 篇文章 0 订阅

前言

本例子不是实际的生产模型,只是为了更好研究死锁而举的例子

需求

james 转账给 jay 20 元,需要保证转账过程原子性操作

能加锁的前提

每个对象都有一个monitor,用于维护自身的锁状态,换句话说,所有对象都可以作为一把锁
每个用户的monitor就可以作为一把锁,并且这把锁是互斥的,暂且称作读写锁

用加锁实现原子性

  1. 锁规则
    每个用户都持有账户的一把读写锁,可以自己持有,也可以被别人拿到
  2. 转账中的加锁
    转出用户需要扣住自己账户的读写锁,并且拿到转入用户的读写锁,也就是转出方需要拿到两把锁
  3. 加锁的目的
    同一笔交易,只能同时成功或者同时失败
    james 扣去的钱 必须等于 jay 增加的钱
    多个交易互相独立

代码演示

public class DeadLockByTransferMoney implements Runnable{

    private static class User {
        String username;
        Integer money;

        public User(String username, Integer money) {
            this.username = username;
            this.money = money;
        }

        public String getUsername() {
            return username;
        }

        public void setUsername(String username) {
            this.username = username;
        }

        public Integer getMoney() {
            return money;
        }

        public void setMoney(Integer money) {
            this.money = money;
        }

        public void transfer(User user, Integer money) {
        	try{
           		this.money = this.money - money;
           		user.money = user.money + money; 
            } catch (自定义异常) {
            	// 回滚逻辑
            }
            System.out.println("转账发生");
        }
    }

    // 转账发起者
    private final User fromLock;
    // 转账接收者
    private final User toLock;

    public DeadLockByTransferMoney(User fromLock, User toLock) {
        this.fromLock = fromLock;
        this.toLock = toLock;
    }

    /**
     * 保证原子性的锁操作
     * 也证明了锁的可重入性质
     */
    @Override
    public void run() {
        synchronized (fromLock) {
            // 模拟网络开销 , 一旦有网络开销, 发生死锁的概率极高
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (toLock) {
                toLock.transfer(toLock, 20);
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        User jay = new User("jay", 500);
        User james = new User("james", 500);
        DeadLockByTransferMoney r1 = new DeadLockByTransferMoney(jay, james);
        DeadLockByTransferMoney r2 = new DeadLockByTransferMoney(james, jay);

        Thread t1 = new Thread(r1);
        Thread t2 = new Thread(r2);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(jay.getMoney() + ":::::" + james.getMoney());
    }
}

死锁分析

两个线程都扣住了自己的锁,为了建立交易的连接,互相等待对方释放锁,造成死锁

发生死锁的总结

  1. 加锁是实现原子性的途径之一
  2. 锁的操作顺序是导致死锁的原因之一
  3. synchronize锁是可重入的

检测死锁

方案一
  1. jdk 自带命令行工具jstack

    jdk安装目录/bin/jstack.exe 文件可以打印出栈信息

  2. 获取当前线程的系统pid
    在这里插入图片描述

  3. cmd 命令,键入/bin 目录下
    jstack -F (资源管理器重的pid)

  4. 查看输出
    在这里插入图片描述

    1. 死锁信息
      在这里插入图片描述
方案二
  1. 工具类获取死锁信息
	        // 获取检测死锁的工具类
            ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
            long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
            if (deadlockedThreads != null && deadlockedThreads.length > 0) {
                for (int k = 0; k < deadlockedThreads.length; k++) {
                    ThreadInfo threadInfo = threadMXBean.getThreadInfo(deadlockedThreads[k]);
                    System.out.println("发生线程死锁了, 相关线程" + threadInfo.getThreadName());
                    isHasDeadLock = true;
                }
            }
            if (isHasDeadLock) {
                break;
            }
  1. 拓展 ,模拟多用户同时转账,也能检测死锁
	/**
 * @Author james
 * @Description
 *
 * 工具类及时反馈是否有死锁
 * @see ManagementFactory#getThreadMXBean 获取工具类
 * @see ThreadMXBean#findDeadlockedThreads() 获取所有死锁的线程id
 * @see ThreadMXBean#getThreadInfo 根据 id 获取线程信息
 *
 * 调整加锁的顺序就能避免死锁的发生
 *
 * @Date 2019/11/22
 */
public class CheckDeadLockAndFix implements Runnable{

    private final static int NUM_USER_COUNT = 500;

    private final static int NUM_THREAD_COUNT = 100000;

    private final static int MONEY_PER_TRANSFER = 1;

    private static class User {
        String username;
        Integer money;

        public User(String username, Integer money) {
            this.username = username;
            this.money = money;
        }

        public User(Integer money) {
            this.money = money;
        }

        public String getUsername() {
            return username;
        }

        public void setUsername(String username) {
            this.username = username;
        }

        public Integer getMoney() {
            return money;
        }

        public void setMoney(Integer money) {
            this.money = money;
        }

        public void transfer(User user, Integer money) {
            this.money = this.money - money;
            user.money = user.money + money;
            // System.out.println("转账发生, 线程: " + Thread.currentThread().getName());
        }
    }

    // 转账发起者
    private final User fromLock;
    // 转账接收者
    private final User toLock;

    public CheckDeadLockAndFix(User fromLock, User toLock) {
        this.fromLock = fromLock;
        this.toLock = toLock;
    }

    /**
     * 保证原子性的锁操作
     * 也证明了锁的可重入性质
     */
    @Override
    public void run() {
        int fromLockHash = System.identityHashCode(fromLock);
        int toLockHash = System.identityHashCode(toLock);

        if (fromLockHash < toLockHash) {
            synchronized (fromLock) {
                // 网络开销极低的情况
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (toLock) {
                    toLock.transfer(toLock, MONEY_PER_TRANSFER);
                }
            }
        } else {
            synchronized (toLock) {
                // 网络开销极低的情况
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (fromLock) {
                    toLock.transfer(toLock, MONEY_PER_TRANSFER);
                }
            }
        }

    }

    public static void main(String[] args) throws InterruptedException {

        Random random = new Random();
        User[] users = new User[NUM_USER_COUNT];
        for (int i = 0; i < NUM_USER_COUNT; i++) {
            users[i]= new User(500);
        }

        boolean isHasDeadLock = false;
        for (int i = 0; i < NUM_THREAD_COUNT; i++) {
            CheckDeadLockAndFix r = new CheckDeadLockAndFix(users[random.nextInt(NUM_USER_COUNT)], users[random.nextInt(NUM_USER_COUNT)]);
            Thread thread = new Thread(r);
            thread.start();
            // 获取检测死锁的工具类
            ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
            long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
            if (deadlockedThreads != null && deadlockedThreads.length > 0) {
                for (int k = 0; k < deadlockedThreads.length; k++) {
                    ThreadInfo threadInfo = threadMXBean.getThreadInfo(deadlockedThreads[k]);
                    System.out.println("发生线程死锁了, 相关线程" + threadInfo.getThreadName());
                    isHasDeadLock = true;
                }
            }
            if (isHasDeadLock) {
                break;
            }
        }
    }
}

代码层面解决死锁问题

思路

避免锁的互相等待

拆分思路
  1. 按顺序拿锁
    让同时获得两个锁的线程按相同的顺序去竞争锁,
    也就是 线程 A 和 B 只能首先竞争 锁1,
    若线程 A 先拿到锁1, B 只能等待 A 线程释放锁才能继续执行,而不是去拿锁2

  2. 确定顺序 – 使用哈希值
    System.identityHashCode(lock);
    每个锁的哈希值是唯一的,大小关系也就确定了,完整的代码:

        if (fromLockHash < toLockHash) {
            synchronized (fromLock) {
                // 网络开销极低的情况
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (toLock) {
                    toLock.transfer(toLock, MONEY_PER_TRANSFER);
                }
            }
        } else if (fromLockHash > toLockHash){
            synchronized (toLock) {
                // 网络开销极低的情况
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (fromLock) {
                    toLock.transfer(toLock, MONEY_PER_TRANSFER);
                }
            }
        }
    
  3. 解决哈希冲突 – 方案一:加多一把锁
    private final static Object lock = new Object();

    	 else {
            // 哈希冲突的情况下,相同哈希值的线程串行完成即可,先竞争到锁的先执行
            synchronized (lock) {
                synchronized (fromLock) {
                    synchronized (toLock) {
                        toLock.transfer(toLock, MONEY_PER_TRANSFER);
                    }
                }
            }
        }
    
  4. 解决哈希冲突 – 方案二:使用数据库数据的主键来完成

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值