Java多线程中的死锁问题

Java程序基本都要涉及到多线程,而在多线程环境中不可避免的要遇到线程死锁的问题。Java不像数据库那么可以检测到死锁,而后进行处理,Java中的死锁问题,只能经过程序员本身写代码时避免引入死锁的可能性来解决。程序员

1. Java中致使死锁的缘由数据库

Java中死锁最简单的状况是,一个线程T1持有锁L1而且申请得到锁L2,而另外一个线程T2持有锁L2而且申请得到锁L1,由于默认的锁申请操做都是阻塞的,因此线程T1和T2永远被阻塞了。致使了死锁。这是最容易理解也是最简单的死锁的形式。可是实际环境中的死锁每每比这个复杂的多。可能会有多个线程造成了一个死锁的环路,好比:线程T1持有锁L1而且申请得到锁L2,而线程T2持有锁L2而且申请得到锁L3,而线程T3持有锁L3而且申请得到锁L1,这样致使了一个锁依赖的环路:T1依赖T2的锁L2,T2依赖T3的锁L3,而T3依赖T1的锁L1。从而致使了死锁。编程

从这两个例子,咱们能够得出结论,产生死锁可能性的最根本缘由是:线程在得到一个锁L1的状况下再去申请另一个锁L2,也就是锁L1想要包含了锁L2,也就是说在得到了锁L1,而且没有释放锁L1的状况下,又去申请得到锁L2,这个是产生死锁的最根本缘由。另外一个缘由是默认的锁申请操做是阻塞的多线程

2. Java中如何避免死锁并发

既然咱们知道了产生死锁可能性的缘由,那么就能够在编码时进行规避。Java是面向对象的编程语言,程序的最小单元是对象,对象封装了数据和操做,因此Java中的锁通常也是以对象为单位的,对象的内置锁保护对象中的数据的并发访问。因此若是咱们可以避免在对象的同步方法中调用其它对象的同步方法,那么就能够避免死锁产生的可能性。以下所示的代码,就存在死锁的可能性:编程语言

public class ClassB {
    private String address;
    // ...
    
    public synchronized void method1(){
        // do something
    }
// ... ... }
public class ClassA {
    private int id;
    private String name;
    private ClassB b;
    // ...
    
    public synchronized void m1(){
        // do something
        b.method1();
    }
    // ... ... }

上面的ClassA.m1()方法,在对象的同步方法中又调用了ClassB的同步方法method1(),因此存在死锁发生的可能性。咱们能够修改以下,避免死锁:this

public class ClassA {
    private int id;
    private String name;
    private ClassB b;
    // ...
    
    public void m2(){
        synchronized(this){
            // do something
        }
        b.method1();
    }
    // ... ...
}

这样的话减少了锁定的范围,两个锁的申请就没有发生交叉,避免了死锁的可能性,这是最理性的状况,由于锁没有发生交叉。可是有时是不容许咱们这样作的。此时,若是只有ClassA中只有一个m1这样的方法,须要同时得到两个对象上的锁,而且不会将实例属性 b 溢出(return b;),而是将实例属性 b 封闭在对象中,那么也不会发生死锁。由于没法造成死锁的闭环。可是若是ClassA中有多个方法须要同时得到两个对象上的锁,那么这些方法就必须以相同的顺序得到锁。编码

好比银行转帐的场景下,咱们必须同时得到两个帐户上的锁,才能进行操做,两个锁的申请必须发生交叉。这时咱们也能够打破死锁的那个闭环,在涉及到要同时申请两个锁的方法中,老是以相同的顺序来申请锁,好比老是先申请 id 大的帐户上的锁 ,而后再申请 id 小的帐户上的锁,这样就没法造成致使死锁的那个闭环。spa

public class Account {
    private int id;    // 主键
    private String name;
    private double balance;
    
    public void transfer(Account from, Account to, double money){
        if(from.getId() > to.getId()){
            synchronized(from){
                synchronized(to){
                    // transfer
                }
            }
        }else{
            synchronized(to){
                synchronized(from){
                    // transfer
                }
            }
        }
    }

    public int getId() {
        return id;
    }
}

这样的话,即便发生了两个帐户好比 id=1的和id=100的两个帐户相互转帐,由于不论是哪一个线程先得到了id=100上的锁,另一个线程都不会去得到id=1上的锁(由于他没有得到id=100上的锁),只能是哪一个线程先得到id=100上的锁,哪一个线程就先进行转帐。这里除了使用id以外,若是没有相似id这样的属性能够比较,那么也可使用对象的hashCode()的值来进行比较。线程

上面咱们说到,死锁的另外一个缘由是默认的锁申请操做是阻塞的,因此若是咱们不使用默认阻塞的锁,也是能够避免死锁的。咱们可使用ReentrantLock.tryLock()方法,在一个循环中,若是tryLock()返回失败,那么就释放以及得到的锁,并睡眠一小段时间。这样就打破了死锁的闭环。

好比:线程T1持有锁L1而且申请得到锁L2,而线程T2持有锁L2而且申请得到锁L3,而线程T3持有锁L3而且申请得到锁L1

此时若是T3申请锁L1失败,那么T3释放锁L3,并进行睡眠,那么T2就能够得到L3了,而后T2执行完以后释放L2, L3,因此T1也能够得到L2了执行完而后释放锁L1, L2,而后T3睡眠醒来,也能够得到L1, L3了。打破了死锁的闭环。

这些状况,都仍是比较好处理的,由于它们都是相关的,咱们很容易意识到这里有发生死锁的可能性,从而能够加以防备。不少状况的场景都不会很明显的让咱们察觉到会存在发生死锁的可能性。因此咱们仍是要注意:

一旦咱们在一个同步方法中,或者说在一个锁的保护的范围中,调用了其它对象的方法时,就要十而分的当心

1)若是其它对象的这个方法会消耗比较长的时间,那么就会致使锁被咱们持有了很长的时间;

2)若是其它对象的这个方法是一个同步方法,那么就要注意避免发生死锁的可能性了;

最好是可以避免在一个同步方法中调用其它对象的延时方法和同步方法。若是不能避免,就要采起上面说到的编码技巧,打破死锁的闭环,防止死锁的发生。同时咱们还能够尽可能使用“不可变对象”来避免锁的使用,在某些状况下还能够避免对象的共享,好比 new 一个新的对象代替共享的对象,由于锁通常是对象上的,对象不相同了,也就能够避免死锁,另外尽可能避免使用静态同步方法,由于静态同步至关于全局锁。还有一些封闭技术可使用:好比堆栈封闭,线程封闭,ThreadLocal,这些技术能够减小对象的共享,也就减小了死锁的可能性。

总结一下

     死锁的根本缘由1)是多个线程涉及到多个锁,这些锁存在着交叉,因此可能会致使了一个锁依赖的闭环;2)默认的锁申请操做是阻塞的因此要避免死锁,就要在一遇到多个对象锁交叉的状况,就要仔细审查这几个对象的类中的全部方法,是否存在着致使锁依赖的环路的可能性。要采起各类方法来杜绝这种可能性。

相关文章
相关标签/搜索