- Java并發編程之美
- 翟陸續 薛賓田
- 1894字
- 2019-07-25 11:54:01
1.9 線程死鎖
1.9.1 什么是線程死鎖
死鎖是指兩個或兩個以上的線程在執行過程中,因爭奪資源而造成的互相等待的現象,在無外力作用的情況下,這些線程會一直相互等待而無法繼續運行下去,如圖1-2所示。

圖1-2
在圖1-2中,線程A已經持有了資源2,它同時還想申請資源1,線程B已經持有了資源1,它同時還想申請資源2,所以線程1和線程2就因為相互等待對方已經持有的資源,而進入了死鎖狀態。
那么為什么會產生死鎖呢?學過操作系統的朋友應該都知道,死鎖的產生必須具備以下四個條件。
● 互斥條件:指線程對已經獲取到的資源進行排它性使用,即該資源同時只由一個線程占用。如果此時還有其他線程請求獲取該資源,則請求者只能等待,直至占有資源的線程釋放該資源。
● 請求并持有條件:指一個線程已經持有了至少一個資源,但又提出了新的資源請求,而新資源已被其他線程占有,所以當前線程會被阻塞,但阻塞的同時并不釋放自己已經獲取的資源。
● 不可剝奪條件:指線程獲取到的資源在自己使用完之前不能被其他線程搶占,只有在自己使用完畢后才由自己釋放該資源。
● 環路等待條件:指在發生死鎖時,必然存在一個線程—資源的環形鏈,即線程集合{T0, T1, T2, …, Tn}中的T0正在等待一個T1占用的資源,T1正在等待T2占用的資源,……Tn正在等待已被T0占用的資源。
下面通過一個例子來說明線程死鎖。
public class DeadLockTest2 { // 創建資源 private static Object resourceA = new Object(); private static Object resourceB = new Object(); public static void main(String[] args) { // 創建線程A Thread threadA = new Thread(new Runnable() { public void run() { synchronized (resourceA) { System.out.println(Thread.currentThread() + " get ResourceA"); 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 esourceB"); } } } }); // 創建線程B Thread threadB = new Thread(new Runnable() { public void run() { synchronized (resourceB) { System.out.println(Thread.currentThread() + " get ResourceB"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread() + "waiting get esourceA"); synchronized (resourceA) { System.out.println(Thread.currentThread() + "get ResourceA"); } }; } }); // 啟動線程 threadA.start(); threadB.start(); } }
輸出結果如下。

下面分析代碼和結果:Thread-0是線程A, Thread-1是線程B,代碼首先創建了兩個資源,并創建了兩個線程。從輸出結果可以知道,線程調度器先調度了線程A,也就是把CPU資源分配給了線程A,線程A使用synchronized(resourceA)方法獲取到了resourceA的監視器鎖,然后調用sleep函數休眠1s,休眠1s是為了保證線程A在獲取resourceB對應的鎖前讓線程B搶占到CPU,獲取到資源resourceB上的鎖。線程A調用sleep方法后線程B會執行synchronized(resourceB)方法,這代表線程B獲取到了resourceB對象的監視器鎖資源,然后調用sleep函數休眠1s。好了,到了這里線程A獲取到了resourceA資源,線程B獲取到了resourceB資源。線程A休眠結束后會企圖獲取resourceB資源,而resourceB資源被線程B所持有,所以線程A會被阻塞而等待。而同時線程B休眠結束后會企圖獲取resourceA資源,而resourceA資源已經被線程A持有,所以線程A和線程B就陷入了相互等待的狀態,也就產生了死鎖。下面談談本例是如何滿足死鎖的四個條件的。
首先,resourceA和resourceB都是互斥資源,當線程A調用synchronized(resourceA)方法獲取到resourceA上的監視器鎖并釋放前,線程B再調用synchronized(resourceA)方法嘗試獲取該資源會被阻塞,只有線程A主動釋放該鎖,線程B才能獲得,這滿足了資源互斥條件。
線程A首先通過synchronized(resourceA)方法獲取到resourceA上的監視器鎖資源,然后通過synchronized(resourceB)方法等待獲取resourceB上的監視器鎖資源,這就構成了請求并持有條件。
線程A在獲取resourceA上的監視器鎖資源后,該資源不會被線程B掠奪走,只有線程A自己主動釋放resourceA資源時,它才會放棄對該資源的持有權,這構成了資源的不可剝奪條件。
線程A持有objectA資源并等待獲取objectB資源,而線程B持有objectB資源并等待objectA資源,這構成了環路等待條件。所以線程A和線程B就進入了死鎖狀態。
1.9.2 如何避免線程死鎖
要想避免死鎖,只需要破壞掉至少一個構造死鎖的必要條件即可,但是學過操作系統的讀者應該都知道,目前只有請求并持有和環路等待條件是可以被破壞的。
造成死鎖的原因其實和申請資源的順序有很大關系,使用資源申請的有序性原則就可以避免死鎖,那么什么是資源申請的有序性呢?我們對上面線程B的代碼進行如下修改。
// 創建線程B Thread threadB = new Thread(new Runnable() { public void run() { synchronized (resourceA) { System.out.println(Thread.currentThread() + " get ResourceB"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread() + "waiting get ResourceA"); synchronized (resourceB) { System.out.println(Thread.currentThread() + "get ResourceA"); } }; } });
輸出結果如下。

如上代碼讓在線程B中獲取資源的順序和在線程A中獲取資源的順序保持一致,其實資源分配有序性就是指,假如線程A和線程B都需要資源1,2,3, ..., n時,對資源進行排序,線程A和線程B只有在獲取了資源n-1時才能去獲取資源n。
我們可以簡單分析一下為何資源的有序分配會避免死鎖,比如上面的代碼,假如線程A和線程B同時執行到了synchronized(resourceA),只有一個線程可以獲取到resourceA上的監視器鎖,假如線程A獲取到了,那么線程B就會被阻塞而不會再去獲取資源B,線程A獲取到resourceA的監視器鎖后會去申請resourceB的監視器鎖資源,這時候線程A是可以獲取到的,線程A獲取到resourceB資源并使用后會放棄對資源resourceB的持有,然后再釋放對resourceA的持有,釋放resourceA后線程B才會被從阻塞狀態變為激活狀態。所以資源的有序性破壞了資源的請求并持有條件和環路等待條件,因此避免了死鎖。
- Python深度學習
- Java程序設計與實踐教程(第2版)
- Magento 1.8 Development Cookbook
- Nginx Essentials
- Visual Foxpro 9.0數據庫程序設計教程
- R語言:邁向大數據之路(加強版)
- Oracle實用教程
- OpenCV with Python Blueprints
- 玩轉.NET Micro Framework移植:基于STM32F10x處理器
- Java程序設計基礎(第6版)
- Web前端開發最佳實踐
- JavaEE架構與程序設計
- 關系數據庫與SQL Server 2012(第3版)
- Docker on Windows
- 樹莓派開發從零開始學:超好玩的智能小硬件制作書