死磕Java多线程 ---- 线程死锁

1. 什么是死锁?

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成互相等待的现象,在无外力作用的情况下,这些线程会相互等待而无法继续运行下去。

在这里插入图片描述
线程T1已经持有了资源R1,他同时还想申请资源R2,与此同时,线程T2已经持有了资源R2,同时还想申请资源R1,所以线程T1和T2就因为相互等待对方已经持有的资源,而进入了死锁状态。

2. 死锁产生的必要条件

产生死锁必须同时满足以下四个条件,只要其中任一条件不成立,死锁就不会发生。

互斥条件:进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某 资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。

不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程自己来释放(只能是主动释放)。

请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源 已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。

循环等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被 链中下一个进程所请求。即存在一个处于等待状态的进程集合{Pl, P2, …, pn},其中Pi等 待的资源被P(i+1)占有(i=0, 1, …, n-1),Pn等待的资源被P0占有

下面来通过一个例子来说明线程死锁

public class StringTest {

    private static  Object resourceA = new Object();
    private static  Object resourceB = new Object();
    
    public static void main(String[] args) {

        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (resourceA){
                    System.out.println(Thread.currentThread() + "get RescourceA");

                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread() + "waiting get sourceB");
                    synchronized (resourceB){
                        System.out.println(Thread.currentThread() + "get RescourceB");
                    }
                }
            }
        });

        Thread threadB = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (resourceB){
                    System.out.println(Thread.currentThread() + "get RescourceB");

                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread() + "waiting get sourceA");
                    synchronized (resourceA){
                        System.out.println(Thread.currentThread() + "get RescourceA");
                    }
                }
            }
        });
        
        threadA.start();
        threadB.start();

    }    
}

让我们来分析一下它是如何满足死锁的四个条件的

首先,resourceA和resourceB都是互斥资源,当线程A调用 synchronized (resourceA)方法获取到resourceA上的监视器锁并释放前,线程B在调用 synchronized (resourceA)方法尝试获取该资源会被阻塞,只有线程A主动释放该锁,线程B才能获得,这满足了资源互斥条件。

线程A首先通过 synchronized (resourceA)方法获取到resourceA上的监视器锁资源,然后通过synchronized (resourceB)方法等待获取resourceB上监视器锁资源, 这就构成了请求并持有条件。

线程A在获取resourceA上的监视器资源后,该资源不会被线程B掠夺走,只有线程A自己主动释放resourceA资源时, 它才会放弃对该资源的持有权,这构成了资源的不可剥夺条件。

线程A持有资源A并等待获取资源B,而线程B持有资源B并等待获取资源A,这构成了环路等待条件。

所以线程A和线程B就进入了死锁状态。

3. 如何避免死锁

想要避免死锁,只需要破坏掉至少一个构造死锁的必要条件即可,但是学过操作系统的都应该知道,目前只有请求并持有和环路等待条件是可以被破坏的。

造成死锁的原因其实和申请资源的顺序有很大的关系,使用资源申请的有序性原则就可以避免死锁。而资源分配有序性就是指,假如多个线程都需要资源1,2,3,.。。。,n时,对资源进行排序,线程只有获取了资源N-1时才能去获取资源N。因为资源的有序性破坏了资源的请求并持有条件和环路等待条件,因此避免了死锁。

4. 死锁检测

死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。

每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。

当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。例如,线程A请求锁7,但是锁7这个时候被线程B持有,这时线程A就可以检查一下线程B是否已经请求了线程A当前所持有的锁。如果线程B确实有这样的请求,那么就是发生了死锁(线程A拥有锁1,请求锁7;线程B拥有锁7,请求锁1)。

当然,死锁一般要比两个线程互相持有对方的锁这种情况要复杂的多。线程A等待线程B,线程B等待线程C,线程C等待线程D,线程D又在等待线程A。线程A为了检测死锁,它需要递进地检测所有被B请求的锁。从线程B所请求的锁开始,线程A找到了线程C,然后又找到了线程D,发现线程D请求的锁被线程A自己持有着。这是它就知道发生了死锁。

快捷工具 -----》使用JDK自带图形化工具JConsole也可以检查是否死锁

发布了45 篇原创文章 · 获赞 3 · 访问量 2306

猜你喜欢

转载自blog.csdn.net/weixin_44046437/article/details/100168690