死锁问题

21 篇文章 0 订阅
3 篇文章 0 订阅

根据不少同学的面试反馈,最近阿里和字节跳动面试时都对多线程相关的问题进行了重点考察,并且面试官都问到了死锁问题。如字节跳动考察的问题是:

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

本文便就此问题进行分析,将用尽可能通俗的语言由浅入深地帮助大家理解死锁,了解其产生的原理与对应的解决方案。

 

什么是线程死锁

线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。当线程进入对象的 synchronized 代码块时,便占有了资源,直到它退出该代码块或者调用 wait 方法,才释放资源,在此期间,其他线程将不能进入该代码块。当线程互相持有对方所需要的资源时,会互相等待对方释放资源,如果线程都不主动释放所占有的资源,将产生死锁。

 

死锁如何产生

# 场景一

星期日早上十点半,你在公路上开车,这是一条窄路,只能容纳一辆车。这时,迎面又驶来一辆车,你们都走到一半,谁也不想倒回去,于是各不相让,陷入无尽的等待。

# 场景二

你和她吵架了,谁也不理谁,甚至晚饭时间都各自煮饭。你在炒京酱肉丝,她在做葱烤鲫鱼。炒到一半你发现小葱被她全部拿走了,于是你默默等待她做好菜后再去拿,殊不知她也在等待你炒完菜后来拿酱油。

# 场景三

你和四个好朋友坐在圆形餐桌旁,你们只做两件事情:吃饭,或者思考。吃饭的时候,你们就停止思考。思考的时候,也停止吃饭。每个人面前有一碗兰州拉面,并且每个人左右两边各有一根筷子。你们必须要拿到两根筷子才能开始吃面。吃完后再放下筷子,让别人可以使用。吃了一会之后,每个人都拿起了自己左手边的筷子,导致每个人都只有一根筷子,并且等待别人吃完放下筷子给自己。可惜,没有人吃到面,所以没有人会放下筷子。(著名的哲学家就餐问题)

# 场景四

你有两个线程 A 和 B ,各自在加锁的状态下运行。A 持有一部分资源,并且等待 B 线程中的资源以完成自己的工作,而此时 B 线程也在等待 A 中的资源以完成自己的工作。由于他们都是锁定状态,所以他们必须完成了自己的工作后,自己持有的资源才能释放。于是线程无休止地等待,导致死锁。

public class JavaTest {
    @Test
    public void test() {
        final Object lockA = new Object();
        final Object lockB = new Object();
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lockA) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (lockB) {
                    }
                    System.out.println("finish A");
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lockB) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (lockA) {
                    }
                    System.out.println("finish B");
                }
            }
        }).start();
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

此程序中,线程 A 持有 lockA 对象,并请求 lockB 对象;线程 B 持有 lockB 对象,并请求 lockA 对象。由于他们都在等待对方释放资源,所以会产生死锁。运行程序,将发现控制台无法打印出 "finish A" 和 "finish B" 消息。

这些都是程序员在工作或生活中会遇到的问题,人生就像是一个进程,时间是我们的主线程,期间做的每一件事都是开启的一个子线程。当多件事冲突时,并发问题就产生了。上述问题都指向同一类并发问题:死锁

产生死锁的的四个条件如下

1、互斥条件:一个资源每次只能被一个进程使用;

2、请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放;

3、不剥夺条件:进程已获得的资源,在没使用完之前,不能强行剥夺;

4、循环等待条件:多个进程之间形成一种互相循环等待资源的关系。

并发带来压力,有的人或有的程序,会因为承受不住压力而崩溃,情绪崩溃和程序崩溃没什么两样。当然,不论是做人还是写程序,面对问题时,正确的做法都应是采取策略,解除死锁。

 

如何避免线程死锁

# 方案一

你想起书中所言:退一步海阔天空。但你也深知公平好过忍让。正值周赛时间,你摇下车窗,对对面的兄弟喊道:咱来比赛一场力扣周赛,谁输了谁倒出去让另一个人过吧!于是你们打开力扣,开始答题。半小时后,你凭借高超的代码水平 AC 了全部题目。对面司机对你拱手道:技不如人,甘拜下风。于是他倒了回去,让出了自己的一半路。最终你们都得以顺利通行。

# 方案二

你在炒菜时发现没有小葱,于是你换位思考,想到她会不会也缺少自己用着的材料。虽然她还在和你冷战,但你劝解自己一个大老爷们不应该和女孩子置气,于是你主动把自己用着的所有材料拿给了她。她感受到你设身处地为她着想,大为感动,你们和好如初。之后她为你们两个人一起炒了京酱肉丝和葱烤鲫鱼。

# 方案三

你和你的朋友们决定给筷子编上号:1~5。规定每个人拿筷子时必须先拿到自己两边的筷子中号码小的那一根,再去拿号码大的那一根。如果小的那一根没有拿到,不能先拿大的。当你们开始吃饭时,由于数字 5 不可能被一个人单独拿到。因为他旁边的另一根筷子编号必定比 5 小,所以不会再出现每个人都拿着一根的无限等待情形。

# 方案四

你在运行两个线程前,预先将线程 A 和 B 中的资源拷贝一份,让他们不需互相等待对方的资源,于是两个线程都得以顺利运行。这四种方案分别破坏了上述四个条件之一。这也是解决死锁问题的四种方法:1、破坏不剥夺条件:让对面的司机放弃了自己已有的资源。2、破坏请求与保持条件:在自己需要的材料缺少时,主动放弃自己持有的资源,防止出现互相等待。3、破坏循环等待条件:由于筷子指定了编号和获取规则,所以每个锁定状态都将按照顺序执行,于是便杜绝了环路等待条件。4、破坏互斥条件:由于每次使用时都拷贝一份,所以一个资源可以被多个进程使用。

 

常考面试考点

面试中对死锁的考察无外乎以上三种类型。死锁的概念和死锁的四个产生条件是固定的,上文都已经讲到。但解决方案并不止以上列出的四种。

事实上,使用预先拷贝资源解决死锁问题的方案一般并不常用。这是由于拷贝的成本往往很大,并且影响效率。实际工作中较常采用的是第三种方案,通过控制加锁顺序解决死锁:

  • 加锁顺序:当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生。如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。当然这种方式需要你事先知道所有可能会用到的锁,然而总有些时候是无法预知的。

除此之外,我们还可以通过设置加锁时限或添加死锁检测避免死锁:

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

其中,死锁检测最出名的算法是由艾兹格·迪杰斯特拉在 1965 年设计的银行家算法,通过记录系统中的资源向量、最大需求矩阵、分配矩阵、需求矩阵,以保证系统只在安全状态下进行资源分配,由此来避免死锁,对于面算法岗的同学一定要对其有所了解。

以上就是力扣为大家整理的死锁相关知识。同时,力扣已经上线了「多线程编程题」,感兴趣的同学赶紧练习吧!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值