Java死锁的原因及解决方法

        要想知道死锁出现的原因和解决方法,首先得知道什么是死锁,死锁是两个或两个以上的运算单元(进程、线程或协程),互相持有对方所需的资源,导致它们都无法向前推进,从而导致永久阻塞的问题。从字面意义上去理解并不太好理解,用实际情况来讨论可以更好的理解。

public synchronized void print(){
    synchronized (this){
        System.out.println("hahha");
    }
}

当代码执行到这个方法,会对该方法进行加锁,避免其他的线程加入进来干扰代码的运行,但是当代码执行到代码块里面时,发现这里又需要加锁,而且加的还是同一个锁(当synchronized对普通方法进行加锁时,相当于对this进行加锁,当对静态方法进行加锁时,相当于对类对象进行加锁),那么就会发生冲突,产生锁竞争。因为该this已被加锁,要想执行下去,就必须对this进行解锁,而要进行解锁,就得代码继续执行下去,这就导致代码逻辑产生矛盾,使得它们各自都无法向前推进,从而导致永久堵塞,这就产生了死锁,也是死锁的一种体现形式,这种死锁一眼就能看出,而方法的调用而导致出现的死锁就让人防不胜防。

public synchronized void func1(){
    func2();
}
public void func2(){
    synchronized (this){};
}

或许现在可以一眼就能发现死锁,但是当调用的方法更多时,还能保证不会出现死锁吗?不过这种情况是不科学的,因为此时进行两次加锁的是同一个线程,为什么说它是不科学的呢?就像你有了一把钥匙,这把钥匙打开了大门,但此时里面还有一扇挂着相同的锁的大门,你就不能用这把钥匙打开下一扇大门了吗?当然可以,因此,当第二次尝试加锁时,该线程已经有了这个锁的权限了,这个时候不应该加锁失败,也不应该阻塞等待。为了应对这种情况,Java也就产生了两种锁,一种是可重入锁,一种是不可重入锁。不可重入锁就是锁不会被保存,当它被某个线程给加了锁后,只要它处于加锁状态时,当它再次收到加锁的请求后,就会拒绝当前加锁,即使是该加锁的请求是对它已经加锁了的线程。而可重入锁则是会保存当前锁,当它被某个线程给加了锁后,当它再次收到加锁的请求,则会对比当前要进行加锁操作的线程和已经加了锁的线程是否一样,这样就可以灵活判断了,如果相同就不进行加锁且直接执行,如果不相同,那就得堵塞了,而synchronized就是可重入锁,因此上述代码不会出现这种情况。如果出现这种情况的话,什么时候进行解锁呢?

public void func2(){
    synchronized (this){
        synchronized (this){
            synchronized (this){
                ……;
            }
        }
    };
}

这很容易想到,当然是要出了最外面的synchronized才能进行解锁,不过我们虽然知道要到最外面的synchronized才解锁,但是jvm怎么知道什么时候才到了最外面的synchronized呢?其实很简单,增加一个计数器就行,只需要在每次进行加锁操作时,让计数器进行加一,每一个出锁操作就让计数器减一,这不就很好的解决了吗?到这死锁的一种典型情况就结束了,那就是一个线程一把锁,但是该锁是不可重入锁,并且一个线程对该锁进行了多次加锁,导致死锁的出现。

        现在就来谈谈死锁的第二种情况:两个线程两把锁,但是这两个线程同时获取对方的锁,这也将导致死锁的发生。

public void func3(){
    synchronized (locker1){
        synchronized (locker2){
            ;
        }
    }
}
public void func4(){
    synchronized (locker2){
        synchronized (locker1){
            ;
        }
    }
}

因为两个线程同时分别调用这两个方法,导致这两把锁同时被加锁,当它们又要再次获取另一把锁时,就会因为该锁被占用而导致线程进入阻塞状态,直到要获取的锁被释放,而因为这两个线程要想进行下去,就必须获取锁,要想获取锁就要进行下去,就如钥匙放在家里,要想进去家里,就得拿钥匙,要想拿钥匙,就得进家里,因而导致死锁的出现。

        而死锁的出现还有第三种情况,那就是N个线程M把锁,比如将一堆人围成一圈吃饭,在每个人的间隔中放一根筷子,而想要吃饭就必须要那两根,并且他们中的人什么时候开始吃饭都是不确定的,而当他们开始吃饭时,拿到筷子,除非吃完否则绝不放下,哪怕只拿到一根,而没拿到的就只能等待有筷子的时候。倘如有些人拿到了两根筷子,那么势必有人只有一根筷子甚至没有筷子,这就造成堵塞,将一跟筷子看成是一把锁,只有获得两把锁的线程才能顺利进行下去,那么那些只有一把锁甚至没有锁的线程就会堵塞,倘如有两个线程出现死锁出现的第二种情况,就会产生死锁,这就是N个线程M把锁出现死锁的情况。

Java死锁的原因及解决方法_第1张图片

        根据死锁出现的三种情况,可以发现其中有着共同之处,可以认为是死锁出现的必要原因。

1)互斥使用:当一个线程获取到一把锁后,其他线程在该线程释放该锁时,是不能获取该锁。

2)不可抢占:锁只能被所有者主动释放,而不能被其他线程给强行释放并直接加锁。

3)请求和保持:当线程获取一把锁后,在获取另一把锁时,会保持对第一把锁的的获取状态。

4)循环等待:t1想要获取locker2,就要等待t2释放locker2, t2想要获取locker1就要等待t1释放locker1,因此它们就得循环等待。

要想出现死锁,上述原因缺一不可。既然知道了死锁出现的原因,那么只需要破坏其中一个原因就可以避免死锁的出现了。因为原因1和原因2是锁的特性,无法修改,那么只能对原因3和4下手了,原因3和原因4取决与代码的结构。不过有时原因3会影响到需求,因此大部分时候对原因4进行改动。比如对所有的锁进行编号,并且规定必须得先对小的加锁,才能对大的加锁,只要代码严格按照该方法进行加锁,那么就可以规避循环等待。

private void func5(){
    synchronized (locker1){
        synchronized (locker3){
            synchronized (locker5){
                ;
            }
        }
    }
}

private void func6(){
    synchronized (locker2){
        synchronized (locker3){
            synchronized (locker4){
                ;
            }
        }
    }
}

到此死锁就此结束。

你可能感兴趣的:(java,开发语言)