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死死的对着,谁也不放过谁。等到了你去重启应用的那一天。。。这个征象就是死锁
死锁的界说:一组相互竞争资源的线程因相互守候,导致“永远”壅闭的征象。
如下图:
Java并发编程实战 04死锁了怎么办?

查找死锁信息

这里我先以一个基本会发生死锁的程序为例,确立两个线程,线程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 Files\Java\jdk1.8.0_221\bin>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 Files\Java\jdk1.8.0_221\bin>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由小到大来排序。照样用转账的例子来注释,代码如下:

python工业互联网应用实战1—SQL与ORM

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/

若是我的文章辅助到您,可以关注我的微信民众号,第一时间分享文章给您
Java并发编程实战 04死锁了怎么办?

原创文章,作者:28qn新闻网,如若转载,请注明出处:https://www.28qn.com/archives/8717.html