什么叫死锁?死锁案例?死锁必须满足哪些条件?如何定位死锁问题?有哪些解决死锁策略?哲学家问题?
1.死鎖是什么?
死鎖一定發(fā)生在并發(fā)環(huán)境中,死鎖是一種狀態(tài),當(dāng)兩個(gè)(或者多個(gè)線程)相互持有對(duì)方所需要的資源,卻又都不主動(dòng)釋放手中持有的資源,導(dǎo)致大家都獲取不到自己想要的資源,所有相關(guān)的線程無法繼續(xù)執(zhí)行。
2.死鎖案例
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors;/*** @author weijie* @date 2020/4/28 10:43*/ public class DeadLockDemo {Object o1 = new Object();Object o2 = new Object();class Task1 implements Runnable{@Overridepublic void run() {synchronized (o1){System.out.println("task1 start ...");String threadName = Thread.currentThread().getName();System.out.print(threadName + "獲取i1對(duì)象鎖--->");/*** 阻塞當(dāng)前線程*/try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (o2){System.out.println("獲取i2對(duì)象鎖");}System.out.println("task2 end ...");}}}class Task2 implements Runnable{@Overridepublic void run() {synchronized (o2) {System.out.println("task2 start ...");String threadName = Thread.currentThread().getName();System.out.print(threadName + "獲取i2對(duì)象鎖--->");synchronized (o1) {System.out.println("獲取i1對(duì)象鎖");}System.out.println("task 2 end ...");}}}public static void main(String[] args) {DeadLockDemo deadLockDemo = new DeadLockDemo();ExecutorService executorService = Executors.newFixedThreadPool(2);executorService.submit(deadLockDemo.new Task1());executorService.submit(deadLockDemo.new Task2());}}3.死鎖必須滿足的四個(gè)條件?
4.如何用命令行和代碼定位死鎖?
1.通過命令行定位
在這里它首先會(huì)打印“Found one Java-level deadlock”,表明“找到了一個(gè)死鎖”。
然后是更詳細(xì)的信息,從中間這部分的信息中可以看出:
t2 線程想要去獲取這個(gè)尾號(hào)為 af0 的鎖對(duì)象,但是它被 t1 線程持有,同時(shí) t2 持有尾號(hào)為 b00 的鎖對(duì)象; 相反,t1 想要獲取尾號(hào)為 b00 的鎖對(duì)象,但是它被 t2 線程持有,同時(shí) t1 持有的卻是尾號(hào)為 af0 的鎖對(duì)象, 這就形成了一個(gè)依賴環(huán)路,發(fā)生了死鎖。 最后它還打印出了“Found 1 deadlock.”,
可以看出,jstack 工具不但幫我們找到了死鎖,甚至還把哪個(gè)線程、想要獲取哪個(gè)鎖、形成什么樣的環(huán)路都告訴我們了,當(dāng)我們有了這樣的信息之后,死鎖就非常容易定位了,所以接下來我們就可以進(jìn)一步修改代碼,來避免死鎖了。
2.代碼定位死鎖
ThreadMXBean用來定位死鎖問題
在main方法中添加如下代碼:
//保證發(fā)生死鎖Thread.sleep(3000);System.out.println("定位死鎖信息");ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();if(deadlockedThreads != null && deadlockedThreads.length > 0){for (int i= 0; i < deadlockedThreads.length; i++){long deadlockedThread = deadlockedThreads[i];ThreadInfo threadInfo = threadMXBean.getThreadInfo(deadlockedThread);System.out.println("線程id為"+threadInfo.getThreadId()+",線程名為" + threadInfo.getThreadName()+"的線程已經(jīng)發(fā)生死鎖,需要的鎖正被線程"+threadInfo.getLockOwnerName()+"持有。");}}
可以看出,ThreadMXBean 也可以幫我們找到并定位死鎖,如果我們?cè)跇I(yè)務(wù)代碼中加入這樣的檢測(cè),那我們就可以在發(fā)生死鎖的時(shí)候及時(shí)地定位,同時(shí)進(jìn)行報(bào)警等其他處理,也就增強(qiáng)了我們程序的健壯性。
5.經(jīng)典的哲學(xué)家問題
1.問題描述
哲學(xué)家就餐問題也被稱為刀叉問題,或者吃面問題。我們先來描述一下這個(gè)問題所要說明的事情,這個(gè)問題如下圖所示:
有 5 個(gè)哲學(xué)家,他們面前都有一雙筷子,即左手有一根筷子,右手有一根筷子。當(dāng)然,這個(gè)問題有多個(gè)版本的描述,可以說是筷子,也可以說是一刀一叉,因?yàn)槌耘E诺臅r(shí)候,需要刀和叉,缺一不可,也有說是用兩把叉子來吃意大利面。這里具體是刀叉還是筷子并不重要,重要的是必須要同時(shí)持有左右兩邊的兩個(gè)才行,也就是說,哲學(xué)家左手要拿到一根筷子,右手也要拿到一根筷子,在這種情況下哲學(xué)家才能吃飯。為了方便理解,我們選取和我國(guó)傳統(tǒng)最貼近的筷子來說明這個(gè)問題。
為什么選擇哲學(xué)家呢?因?yàn)檎軐W(xué)家的特點(diǎn)是喜歡思考,所以我們可以把哲學(xué)家一天的行為抽象為思考,然后吃飯,并且他們吃飯的時(shí)候要用一雙筷子,而不能只用一根筷子。
1. 主流程
我們來看一下哲學(xué)家就餐的主流程。哲學(xué)家如果想吃飯,他會(huì)先嘗試拿起左手的筷子,然后再嘗試拿起右手的筷子,如果某一根筷子被別人使用了,他就得等待他人用完,用完之后他人自然會(huì)把筷子放回原位,接著他把筷子拿起來就可以吃了(不考慮衛(wèi)生問題)。這就是哲學(xué)家就餐的最主要流程。
2. 流程的偽代碼
我們來看一下這個(gè)流程的偽代碼,如下所示:
while(true) { // 思考人生、宇宙、萬物...think();// 思考后感到餓了,需要拿筷子開始吃飯pick_up_left_chopstick();pick_up_right_chopstick();eat();put_down_right_chopstick();put_down_left_chopstick();// 吃完飯后,繼續(xù)思考人生、宇宙、萬物... }while(true) 代表整個(gè)是一個(gè)無限循環(huán)。在每個(gè)循環(huán)中,哲學(xué)家首先會(huì)開始思考,思考一段時(shí)間之后(這個(gè)時(shí)間長(zhǎng)度可以是隨機(jī)的),他感到餓了,就準(zhǔn)備開始吃飯。在吃飯之前必須先拿到左手的筷子,再拿到右手的筷子,然后才開始吃飯;吃完之后,先放回右手的筷子,再放回左手的筷子;由于這是個(gè) while 循環(huán),所以他就會(huì)繼續(xù)思考人生,開啟下一個(gè)循環(huán)。這就是整個(gè)過程。
3.有死鎖和資源耗盡的風(fēng)險(xiǎn)
這里存在什么風(fēng)險(xiǎn)呢?就是發(fā)生死鎖的風(fēng)險(xiǎn)。如下面的動(dòng)畫所示:
根據(jù)我們的邏輯規(guī)定,在拿起左手邊的筷子之后,下一步是去拿右手的筷子。大部分情況下,右邊的哲學(xué)家正在思考,所以當(dāng)前哲學(xué)家的右手邊的筷子是空閑的,或者如果右邊的哲學(xué)家正在吃飯,那么當(dāng)前的哲學(xué)家就等右邊的哲學(xué)家吃完飯并釋放筷子,于是當(dāng)前哲學(xué)家就能拿到了他右手邊的筷子了。
但是,如果每個(gè)哲學(xué)家都同時(shí)拿起左手的筷子,那么就形成了環(huán)形依賴,在這種特殊的情況下,每個(gè)人都拿著左手的筷子,都缺少右手的筷子,那么就沒有人可以開始吃飯了,自然也就沒有人會(huì)放下手中的筷子。這就陷入了死鎖,形成了一個(gè)相互等待的情況。
4.代碼演示
代碼如下所示:
package com.netty.rpc.test.util;import java.lang.management.ManagementFactory; import java.lang.management.ThreadInfo; import java.lang.management.ThreadMXBean;public class DiningPhilosophers {public static class Philosopher implements Runnable {private Object leftChopstick;private Object rightChopstick;public Philosopher(Object leftChopstick, Object rightChopstick) {this.leftChopstick = leftChopstick;this.rightChopstick = rightChopstick;}@Overridepublic void run() {try {while (true) {doAction("思考人生、宇宙、萬物、靈魂...");synchronized (leftChopstick) {doAction("拿起左邊的筷子");synchronized (rightChopstick) {doAction("拿起右邊的筷子");doAction("吃飯");doAction("放下右邊的筷子");}doAction("放下左邊的筷子");}}} catch (InterruptedException e) {e.printStackTrace();}}private void doAction(String action) throws InterruptedException {System.out.println(Thread.currentThread().getName() + " " + action);Thread.sleep((long) (Math.random() * 10));}}public static void main(String[] args) throws InterruptedException {Philosopher[] philosophers = new Philosopher[5];Object[] chopsticks = new Object[philosophers.length];for (int i = 0; i < chopsticks.length; i++) {chopsticks[i] = new Object();}for (int i = 0; i < philosophers.length; i++) {Object leftChopstick = chopsticks[i];Object rightChopstick = chopsticks[(i + 1) % chopsticks.length];philosophers[i] = new Philosopher(rightChopstick, leftChopstick);new Thread(philosophers[i], "哲學(xué)家" + (i + 1) + "號(hào)").start();}Thread.sleep(1000 * 10);ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();if(deadlockedThreads != null && deadlockedThreads.length > 0){for (int i= 0; i < deadlockedThreads.length; i++){long deadlockedThread = deadlockedThreads[i];ThreadInfo threadInfo = threadMXBean.getThreadInfo(deadlockedThread);System.out.println("線程id為"+threadInfo.getThreadId()+",線程名為" + threadInfo.getThreadName()+"的線程已經(jīng)發(fā)生死鎖,需要的鎖正被線程"+threadInfo.getLockOwnerName()+"持有。");}}} }運(yùn)行后截圖:
哲學(xué)家 1、3、2、4、5 幾乎同時(shí)開始思考,然后,假設(shè)他們思考的時(shí)間比較相近,于是他們都在幾乎同一時(shí)刻想開始吃飯,都紛紛拿起左手的筷子,這時(shí)就陷入了死鎖狀態(tài),沒有人可以拿到右手的筷子,也就沒有人可以吃飯,于是陷入了無窮等待,這就是經(jīng)典的哲學(xué)家就餐問題。
5.多種解決方案
對(duì)于這個(gè)問題我們?cè)撊绾谓鉀Q呢?有多種解決方案,這里我們講講其中的幾種。前面我們講過,要想解決死鎖問題,只要破壞死鎖四個(gè)必要條件的任何一個(gè)都可以。
1. 服務(wù)員檢查
第一個(gè)解決方案就是引入服務(wù)員檢查機(jī)制。比如我們引入一個(gè)服務(wù)員,當(dāng)每次哲學(xué)家要吃飯時(shí),他需要先詢問服務(wù)員:我現(xiàn)在能否去拿筷子吃飯?此時(shí),服務(wù)員先判斷他拿筷子有沒有發(fā)生死鎖的可能,假如有的話,服務(wù)員會(huì)說:現(xiàn)在不允許你吃飯。這是一種解決方案。
2. 領(lǐng)導(dǎo)調(diào)節(jié)
我們根據(jù)上一講的死鎖檢測(cè)和恢復(fù)策略,可以引入一個(gè)領(lǐng)導(dǎo),這個(gè)領(lǐng)導(dǎo)進(jìn)行定期巡視。如果他發(fā)現(xiàn)已經(jīng)發(fā)生死鎖了,就會(huì)剝奪某一個(gè)哲學(xué)家的筷子,讓他放下。這樣一來,由于這個(gè)人的犧牲,其他的哲學(xué)家就都可以吃飯了。這也是一種解決方案。
3. 改變一個(gè)哲學(xué)家拿筷子的順序
我們還可以利用死鎖避免策略,那就是從邏輯上去避免死鎖的發(fā)生,比如改變其中一個(gè)哲學(xué)家拿筷子的順序。我們可以讓 4 個(gè)哲學(xué)家都先拿左邊的筷子再拿右邊的筷子,但是有一名哲學(xué)家與他們相反,他是先拿右邊的再拿左邊的,這樣一來就不會(huì)出現(xiàn)循環(huán)等待同一邊筷子的情況,也就不會(huì)發(fā)生死鎖了。
死鎖解決
我們把“改變一個(gè)哲學(xué)家拿筷子的順序”這件事情用代碼來寫一下,修改后的 main 方法如下:
if (i == philosophers.length - 1) ,在這種情況下,我們給它傳入的筷子順序恰好相反,這樣一來,他拿筷子的順序也就相反了,他會(huì)先拿起右邊的筷子,再拿起左邊的筷子。那么這個(gè)程序運(yùn)行的結(jié)果,是所有哲學(xué)家都可以正常地去進(jìn)行思考和就餐了,并且不會(huì)發(fā)生死鎖。
總結(jié)
以上是生活随笔為你收集整理的什么叫死锁?死锁案例?死锁必须满足哪些条件?如何定位死锁问题?有哪些解决死锁策略?哲学家问题?的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: JedisConnectionExcep
- 下一篇: 关于kafka中acks是否可以为all