【Java面试】以故事的形式教你理解死锁,如何避免死锁

什么是死锁

先通过一段产生死锁的代码来理解死锁是怎么产生的。

/**
 * 线程死锁
 *
 */
public class ThreadDeadkockStudy {

    // 钱
    static Object money = new Object();
    // 货
    static Object goods = new Object();

    public static void main(String[] args) {

        // 卖家
        new Thread(new Runnable() {
            public void run() {
            	// 卖家拿着货
                synchronized (goods) {
                    System.out.println("卖家:先钱!");
                    try {
                    	// 我看你怎么说
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace(); 
                    }
                    // 除非你先给钱
                    synchronized (money) {
                        System.out.println("卖家:合作愉快");
                    }
                }
            }
        }).start();

        // 买家
        new Thread(new Runnable() {
            public void run() {
            	// 买家拿着钱
                synchronized (money) {
                    System.out.println("买家:不行,我先验验货!");
                    try {
                    	// 我也看你怎么说
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // 除非你先给我验验货
                    synchronized (goods) {
                        System.out.println("买家:合作愉快");
                    }
                }
            }
        }).start();
    }

}

最后输出的结果是这样的,我们发现一直卡在这里,没有继续往下执行了。

卖家:先钱!
买家:不行,我先验验货!

再用比较通俗的语言去解释这个现象。
黑社会和商人一起到达了约定的交易地点。
商人:“先钱!”
黑社会:“不行,我先验验货!”
商人看不到黑社会的钱,不愿意把手里的货交出去;黑社会怕商人给的假货,也不把钱拿出来,就这样僵住了。

两个不同的线程,分别拥有各自的资源,并且都想拥有对方持有的资源,但是谁也不愿意先妥协,最后陷入了僵局,也就是我们所说的死锁。


死锁触发条件

我们继续看下面的几组代码,看看为什么他们为什么代码类似,但是没有产生死锁呢。

卖家强买强卖

// 卖家强买强卖
public static void sellerFast() {
    // 卖家
    new Thread(new Runnable() {
        public void run() {
            synchronized (goods) {
                System.out.println("卖家:先钱!");
                synchronized (money) {
                    System.out.println("卖家:合作愉快");
                }
            }
        }
    }).start();

    // 买家
    new Thread(new Runnable() {
        public void run() {
            synchronized (money) {
            	System.out.println("买家:不行,我先验验货!");
                synchronized (goods) {
                    System.out.println("买家:合作愉快");
                }
            }
        }
    }).start();
}

再看看输出的结果,由于卖家没有等待买家的回应,就把买家兜里的钱给拿走了,并且把货也硬塞到了自己的兜里,只能吃哑巴亏了。

卖家:先钱!
卖家:合作愉快
买家:不行,我先验验货!
买家:合作愉快

这个产生的原因是,我们线程初始化和启动是需要一点时间的,而第一个线程已经执行完了,第二个线程才刚刚执行到那里的时候,这个锁已经被释放了。

如果两个线程执行的先后顺序对调的情况,我们就可以理解成是:买家看卖家不在店里,但是看到了自己想买的东西,没等卖家回来就把钱放在了柜台上,并且留下了个字条:我看你不在,我把货拿走了,钱我放在柜台了!
最后他们成功的完成了这笔交易。

  1. 互斥条件:进程对于所分配到的资源具有排它性,即一个资源只能被一个进程占用,直到被该进程释放 。
  2. 请求和保持条件:一个进程因请求被占用资源而发生阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:任何一个资源在没被该进程释放之前,任何其他进程都无法对他剥夺占用。
  4. 循环等待条件:当发生死锁时,所等待的进程必定会形成一个环路(类似于死循环),造成永久阻塞。

我们这里没有满足第二条,因为当我们想要取用该资源的时候,资源已经被提早释放了,所以就没有造成死锁。


如何避免死锁

在生活中,一般造成死锁都是由于沟通问题导致的,比如跨行转账也可能出现死锁的情况,但是这完全是可以被避免的。
死锁
如果没有中间机构,我们也可以通过自行妥协的方式,规定一个比较合理的顺序,来避免死锁的诞生。

加锁顺序: 当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生。如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。当然这种方式需要你事先知道所有可能会用到的锁,然而总有些时候是无法预知的。
加锁时限: 加上一个超时时间,若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。但是如果有非常多的线程同一时间去竞争同一批资源,就算有超时和回退机制,还是可能会导致这些线程重复地尝试但却始终得不到锁。
死锁检测: 死锁检测即每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。


参考资料

什么是线程死锁?如何避免死锁?

线程死锁

文章中出现的任何错误欢迎指正,共同进步!

最后做个小小广告,有对WEB开发和网络安全感兴趣的,可以加群一起学习和交流!

交流群
QQ:425343603

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章