zoukankan      html  css  js  c++  java
  • 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死死的对着,谁也不放过谁。等到了你去重启应用的那一天。。。这个现象就是死锁
    死锁的定义:一组互相竞争资源的线程因互相等待,导致“永久”阻塞的现象。
    如下图:
    死锁1.jpg

    查找死锁信息

    这里我先以一个基本会发生死锁的程序为例,创建两个线程,线程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 FilesJavajdk1.8.0_221in>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 FilesJavajdk1.8.0_221in>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/

    如果我的文章帮助到您,可以关注我的微信公众号,第一时间分享文章给您
    微信公众号

  • 相关阅读:
    Navicat 连接MySQL 8.0.11 出现2059错误
    安全技术运营的心得
    浅谈命令混淆
    2021年度总结与2022新的展望
    域环境搭建之安装exchange
    内网ADCS攻防
    CVE202142287复现
    企业安全建设——安全防线框架建设(一)
    frp_v0.37.1内网穿透,内网服务公网用不求人
    WP7XNA 多点触摸
  • 原文地址:https://www.cnblogs.com/Johnson-lin/p/12874009.html
Copyright © 2011-2022 走看看