Java 程序死锁问题原理及解决方案
原文出處: IBM developerWorks
Java 語言通過 synchronized 關(guān)鍵字來保證原子性,這是因為每一個 Object 都有一個隱含的鎖,這個也稱作監(jiān)視器對象。在進入 synchronized 之前自動獲取此內(nèi)部鎖,而一旦離開此方式,無論是完成或者中斷都會自動釋放鎖。顯然這是一個獨占鎖,每個鎖請求之間是互斥的。相對于眾多高級鎖 (Lock/ReadWriteLock 等),synchronized 的代價都比后者要高。但是 synchronzied 的語法比較簡單,而且也比較容易使用和理解。Lock 一旦調(diào)用了 lock() 方法獲取到鎖而未正確釋放的話很有可能造成死鎖,所以 Lock 的釋放操作總是跟在 finally 代碼塊里面,這在代碼結(jié)構(gòu)上也是一次調(diào)整和冗余。Lock 的實現(xiàn)已經(jīng)將硬件資源用到了極致,所以未來可優(yōu)化的空間不大,除非硬件有了更高的性能,但是 synchronized 只是規(guī)范的一種實現(xiàn),這在不同的平臺不同的硬件還有很高的提升空間,未來 Java 鎖上的優(yōu)化也會主要在這上面。既然 synchronzied 都不可能避免死鎖產(chǎn)生,那么死鎖情況會是經(jīng)常容易出現(xiàn)的錯誤,下面具體描述死鎖發(fā)生的原因及解決方法。
死鎖描述
死鎖是操作系統(tǒng)層面的一個錯誤,是進程死鎖的簡稱,最早在 1965 年由 Dijkstra 在研究銀行家算法時提出的,它是計算機操作系統(tǒng)乃至整個并發(fā)程序設(shè)計領(lǐng)域最難處理的問題之一。
事實上,計算機世界有很多事情需要多線程方式去解決,因為這樣才能最大程度上利用資源,才能體現(xiàn)出計算的高效。但是,實際上來說,計算機系統(tǒng)中有很多一次只能由一個進程使用的資源的情況,例如打印機,同時只能有一個進程控制它。在多通道程序設(shè)計環(huán)境中,若干進程往往要共享這類資源,而且一個進程所需要的資源還很有可能不止一個。因此,就會出現(xiàn)若干進程競爭有限資源,又推進順序不當(dāng),從而構(gòu)成無限期循環(huán)等待的局面。我們稱這種狀態(tài)為死鎖。簡單一點描述,死鎖是指多個進程循環(huán)等待它方占有的資源而無限期地僵持下去的局面。很顯然,如果沒有外力的作用,那么死鎖涉及到的各個進程都將永遠處于封鎖狀態(tài)。
系統(tǒng)發(fā)生死鎖現(xiàn)象不僅浪費大量的系統(tǒng)資源,甚至導(dǎo)致整個系統(tǒng)崩潰,帶來災(zāi)難性后果。所以,對于死鎖問題在理論上和技術(shù)上都必須予以高度重視。
銀行家算法
一個銀行家如何將一定數(shù)目的資金安全地借給若干個客戶,使這些客戶既能借到錢完成要干的事,同時銀行家又能收回全部資金而不至于破產(chǎn)。銀行家就像一個操作系統(tǒng),客戶就像運行的進程,銀行家的資金就是系統(tǒng)的資源。
銀行家算法需要確保以下四點:
- 當(dāng)一個顧客對資金的最大需求量不超過銀行家現(xiàn)有的資金時就可接納該顧客
- 顧客可以分期貸款, 但貸款的總數(shù)不能超過最大需求量
- 當(dāng)銀行家現(xiàn)有的資金不能滿足顧客尚需的貸款數(shù)額時,對顧客的貸款可- 推遲支付,但總能使顧客在有限的時間里得到貸款
- 當(dāng)顧客得到所需的全部資金后,一定能在有限的時間里歸還所有的資金
清單 1. 銀行家算法實現(xiàn)
/*一共有5個進程需要請求資源,有3類資源*/ public class BankDemo {/* 每個進程所需要的最大資源數(shù) */public static int MAX[][] = { { 7, 5, 3 }, { 3, 2, 2 }, { 9, 0, 2 },{ 2, 2, 2 }, { 4, 3, 3 } };/* 系統(tǒng)擁有的初始資源數(shù) */public static int AVAILABLE[] = { 10, 5, 7 };/* 系統(tǒng)已給每個進程分配的資源數(shù) */public static int ALLOCATION[][] = { { 0, 0, 0 }, { 0, 0, 0 }, { 0, 0, 0 },{ 0, 0, 0 }, { 0, 0, 0 } };/* 每個進程還需要的資源數(shù) */public static int NEED[][] = { { 7, 5, 3 }, { 3, 2, 2 }, { 9, 0, 2 },{ 2, 2, 2 }, { 4, 3, 3 } };/* 每次申請的資源數(shù) */public static int Request[] = { 0, 0, 0 };/* 進程數(shù)與資源數(shù) */public static int M = 5, N = 3;int FALSE = 0;int TRUE = 1;public void showdata(){int i, j;System.out.print( "系統(tǒng)可用的資源數(shù)為:/n" );for ( j = 0; j < N; j++ ){System.out.print( "資源" + j + ":" + AVAILABLE[j] + " " );}System.out.println();System.out.println( "各進程還需要的資源量:" );for ( i = 0; i < M; i++ ){System.out.print( "進程" + i + ":" );for ( j = 0; j < N; j++ ){System.out.print( "資源" + j + ":" + NEED[i][j] + " " );}System.out.print( "/n" );}System.out.print( "各進程已經(jīng)得到的資源量: /n" );for ( i = 0; i < M; i++ ){System.out.print( "進程" );System.out.print( i );for ( j = 0; j < N; j++ ){System.out.print( "資源" + j + ":" + ALLOCATION[i][j] + " " );}System.out.print( "/n" );}}/* 分配資源,并重新更新各種狀態(tài) */public void changdata( int k ){int j;for ( j = 0; j < N; j++ ){AVAILABLE[j] = AVAILABLE[j] - Request[j];ALLOCATION[k][j] = ALLOCATION[k][j] + Request[j];NEED[k][j] = NEED[k][j] - Request[j];}};/* 回收資源,并重新更新各種狀態(tài) */public void rstordata( int k ){int j;for ( j = 0; j < N; j++ ){AVAILABLE[j] = AVAILABLE[j] + Request[j];ALLOCATION[k][j] = ALLOCATION[k][j] - Request[j];NEED[k][j] = NEED[k][j] + Request[j];}};/* 釋放資源 */public void free( int k ){for ( int j = 0; j < N; j++ ){AVAILABLE[j] = AVAILABLE[j] + ALLOCATION[k][j];System.out.print( "釋放" + k + "號進程的" + j + "資源!/n" );}}public int check0( int k ){int j, n = 0;for ( j = 0; j < N; j++ ){if ( NEED[k][j] == 0 )n++;}if ( n == 3 )return(1);elsereturn(0);}/** 檢查安全性函數(shù)* 所以銀行家算法其核心是:保證銀行家系統(tǒng)的資源數(shù)至少不小于一個客戶的所需要的資源數(shù)。在安全性檢查函數(shù) chkerr() 上由這個方法來實現(xiàn)* 這個循環(huán)來進行核心判斷,從而完成了銀行家算法的安全性檢查工作。*/public int chkerr( int s ){int WORK;int FINISH[] = new int[M], temp[] = new int[M]; /* 保存臨時的安全進程序列 */int i, j, k = 0;for ( i = 0; i < M; i++ )FINISH[i] = FALSE;for ( j = 0; j < N; j++ ){WORK = AVAILABLE[j]; /* 第 j 個資源可用數(shù) */i = s;/* 判斷第 i 個進程是否滿足條件 */while ( i < M ){if ( FINISH[i] == FALSE && NEED[i][j] <= WORK ){WORK = WORK + ALLOCATION[i][j];FINISH[i] = TRUE;temp[k] = i;k++;i = 0;} else {i++;}}for ( i = 0; i < M; i++ )if ( FINISH[i] == FALSE ){System.out.print( "/n 系統(tǒng)不安全!!! 本次資源申請不成功!/n" );return(1);}}System.out.print( "/n 經(jīng)安全性檢查,系統(tǒng)安全,本次分配成功。/n" );System.out.print( "本次安全序列:" );for ( i = 0; i < M - 1; i++ ){System.out.print( "進程" + temp[i] + "->" );}System.out.print( "進程" + temp[M - 1] );System.out.println( "/n" );return(0);} }死鎖示例
死鎖問題是多線程特有的問題,它可以被認(rèn)為是線程間切換消耗系統(tǒng)性能的一種極端情況。在死鎖時,線程間相互等待資源,而又不釋放自身的資源,導(dǎo)致無窮無盡的等待,其結(jié)果是系統(tǒng)任務(wù)永遠無法執(zhí)行完成。死鎖問題是在多線程開發(fā)中應(yīng)該堅決避免和杜絕的問題。
一般來說,要出現(xiàn)死鎖問題需要滿足以下條件:
只要破壞死鎖 4 個必要條件之一中的任何一個,死鎖問題就能被解決。
我們先來看一個示例,前面說過,死鎖是兩個甚至多個線程被永久阻塞時的一種運行局面,這種局面的生成伴隨著至少兩個線程和兩個或者多個資源。代碼清單 2 所示的示例中,我們編寫了一個簡單的程序,它將會引起死鎖發(fā)生,然后我們就會明白如何分析它。
清單 2. 死鎖示例
public class ThreadDeadlock {public static void main(String[] args) throws InterruptedException {Object obj1 = new Object();Object obj2 = new Object();Object obj3 = new Object();Thread t1 = new Thread(new SyncThread(obj1, obj2), "t1");Thread t2 = new Thread(new SyncThread(obj2, obj3), "t2");Thread t3 = new Thread(new SyncThread(obj3, obj1), "t3");t1.start();Thread.sleep(5000);t2.start();Thread.sleep(5000);t3.start();} } class SyncThread implements Runnable {private Object obj1;private Object obj2;public SyncThread(Object o1, Object o2) {this.obj1 = o1;this.obj2 = o2;}@Overridepublic void run() {String name = Thread.currentThread().getName();System.out.println(name + " acquiring lock on " + obj1);synchronized (obj1) {System.out.println(name + " acquired lock on " + obj1);work();System.out.println(name + " acquiring lock on " + obj2);synchronized (obj2) {System.out.println(name + " acquired lock on " + obj2);work();}System.out.println(name + " released lock on " + obj2);}System.out.println(name + " released lock on " + obj1);System.out.println(name + " finished execution.");}private void work() {try {Thread.sleep(30000);} catch (InterruptedException e) {e.printStackTrace();}} }在上面的程序中同步線程正完成 Runnable 的接口,它工作的是兩個對象,這兩個對象向?qū)Ψ綄で笏梨i而且都在使用同步阻塞。在主函數(shù)中,我使用了三個為同步線程運行的線程,而且在其中每個線程中都有一個可共享的資源。這些線程以向第一個對象獲取封鎖這種方式運行。但是當(dāng)它試著向第二個對象獲取封鎖時,它就會進入等待狀態(tài),因為它已經(jīng)被另一個線程封鎖住了。這樣,在線程引起死鎖的過程中,就形成了一個依賴于資源的循環(huán)。當(dāng)我執(zhí)行上面的程序時,就產(chǎn)生了輸出,但是程序卻因為死鎖無法停止。輸出如清單 3 所示。
清單 3. 清單 2 運行輸出
t1 acquiring lock on java.lang.Object@1dd3812 t1 acquired lock on java.lang.Object@1dd3812 t2 acquiring lock on java.lang.Object@c791b9 t2 acquired lock on java.lang.Object@c791b9 t3 acquiring lock on java.lang.Object@1aa9f99 t3 acquired lock on java.lang.Object@1aa9f99 t1 acquiring lock on java.lang.Object@c791b9 t2 acquiring lock on java.lang.Object@1aa9f99在此我們可以清楚地在輸出結(jié)果中辨認(rèn)出死鎖局面,但是在我們實際所用的應(yīng)用中,發(fā)現(xiàn)死鎖并將它排除是非常難的。
死鎖情況診斷
JVM 提供了一些工具可以來幫助診斷死鎖的發(fā)生,如下面程序清單 4 所示,我們實現(xiàn)了一個死鎖,然后嘗試通過 jstack 命令追蹤、分析死鎖發(fā)生。
清單 4. 死鎖代碼
jstack 可用于導(dǎo)出 Java 應(yīng)用程序的線程堆棧,-l 選項用于打印鎖的附加信息。我們運行 jstack 命令,輸出入清單 5 和 6 所示,其中清單 5 里面可以看到線程處于運行狀態(tài),代碼中調(diào)用了擁有鎖投票、定時鎖等候和可中斷鎖等候等特性的 ReentrantLock 鎖機制。清單 6 直接打印出出現(xiàn)死鎖情況,報告 north 和 sourth 兩個線程互相等待資源,出現(xiàn)了死鎖。
清單 5. jstack 運行輸出 1
[root@facenode4 ~]# jstack -l 31274 2015-01-29 12:40:27 Full thread dump Java HotSpot(TM) 64-Bit Server VM (20.45-b01 mixed mode):"Attach Listener" daemon prio=10 tid=0x00007f6d3c001000 nid=0x7a87 waiting on condition [0x0000000000000000]java.lang.Thread.State: RUNNABLELocked ownable synchronizers:- None"DestroyJavaVM" prio=10 tid=0x00007f6da4006800 nid=0x7a2b waiting on condition [0x0000000000000000]java.lang.Thread.State: RUNNABLELocked ownable synchronizers:- None"north" prio=10 tid=0x00007f6da4101800 nid=0x7a47 waiting on condition [0x00007f6d8963b000]java.lang.Thread.State: WAITING (parking)at sun.misc.Unsafe.park(Native Method)- parking to wait for <0x000000075903c7c8> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)at java.util.concurrent.locks.LockSupport.park(LockSupport.java:156)at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:811)at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:867)at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1201)at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:312)at DeadLock.run(DeadLock.java:50)Locked ownable synchronizers:- <0x000000075903c798> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)"south" prio=10 tid=0x00007f6da4100000 nid=0x7a46 waiting on condition [0x00007f6d8973c000]java.lang.Thread.State: WAITING (parking)at sun.misc.Unsafe.park(Native Method)- parking to wait for <0x000000075903c798> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)at java.util.concurrent.locks.LockSupport.park(LockSupport.java:156)at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:811)at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:867)at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1201)at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:312)at DeadLock.run(DeadLock.java:28)Locked ownable synchronizers:- <0x000000075903c7c8> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)"Low Memory Detector" daemon prio=10 tid=0x00007f6da40d2800 nid=0x7a44 runnable [0x0000000000000000]java.lang.Thread.State: RUNNABLELocked ownable synchronizers:- None"C2 CompilerThread1" daemon prio=10 tid=0x00007f6da40d0000 nid=0x7a43 waiting on condition [0x0000000000000000]java.lang.Thread.State: RUNNABLELocked ownable synchronizers:- None"C2 CompilerThread0" daemon prio=10 tid=0x00007f6da40cd000 nid=0x7a42 waiting on condition [0x0000000000000000]java.lang.Thread.State: RUNNABLELocked ownable synchronizers:- None"Signal Dispatcher" daemon prio=10 tid=0x00007f6da40cb000 nid=0x7a41 runnable [0x0000000000000000]java.lang.Thread.State: RUNNABLELocked ownable synchronizers:- None"Finalizer" daemon prio=10 tid=0x00007f6da40af000 nid=0x7a40 in Object.wait() [0x00007f6d89d44000]java.lang.Thread.State: WAITING (on object monitor)at java.lang.Object.wait(Native Method)- waiting on <0x0000000759001300> (a java.lang.ref.ReferenceQueue$Lock)at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:118)- locked <0x0000000759001300> (a java.lang.ref.ReferenceQueue$Lock)at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:134)at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:171)Locked ownable synchronizers:- None"Reference Handler" daemon prio=10 tid=0x00007f6da40ad000 nid=0x7a3f in Object.wait() [0x00007f6d89e45000]java.lang.Thread.State: WAITING (on object monitor)at java.lang.Object.wait(Native Method)- waiting on <0x00000007590011d8> (a java.lang.ref.Reference$Lock)at java.lang.Object.wait(Object.java:485)at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:116)- locked <0x00000007590011d8> (a java.lang.ref.Reference$Lock)Locked ownable synchronizers:- None"VM Thread" prio=10 tid=0x00007f6da40a6000 nid=0x7a3e runnable "GC task thread#0 (ParallelGC)" prio=10 tid=0x00007f6da4019800 nid=0x7a2c runnable "GC task thread#1 (ParallelGC)" prio=10 tid=0x00007f6da401b000 nid=0x7a2d runnable "GC task thread#2 (ParallelGC)" prio=10 tid=0x00007f6da401d000 nid=0x7a2e runnable "GC task thread#3 (ParallelGC)" prio=10 tid=0x00007f6da401f000 nid=0x7a2f runnable "GC task thread#4 (ParallelGC)" prio=10 tid=0x00007f6da4020800 nid=0x7a30 runnable "GC task thread#5 (ParallelGC)" prio=10 tid=0x00007f6da4022800 nid=0x7a31 runnable "GC task thread#6 (ParallelGC)" prio=10 tid=0x00007f6da4024800 nid=0x7a32 runnable "GC task thread#7 (ParallelGC)" prio=10 tid=0x00007f6da4026000 nid=0x7a33 runnable "GC task thread#8 (ParallelGC)" prio=10 tid=0x00007f6da4028000 nid=0x7a34 runnable "GC task thread#9 (ParallelGC)" prio=10 tid=0x00007f6da402a000 nid=0x7a35 runnable "GC task thread#10 (ParallelGC)" prio=10 tid=0x00007f6da402b800 nid=0x7a36 runnable "GC task thread#11 (ParallelGC)" prio=10 tid=0x00007f6da402d800 nid=0x7a37 runnable "GC task thread#12 (ParallelGC)" prio=10 tid=0x00007f6da402f800 nid=0x7a38 runnable "GC task thread#13 (ParallelGC)" prio=10 tid=0x00007f6da4031000 nid=0x7a39 runnable "GC task thread#14 (ParallelGC)" prio=10 tid=0x00007f6da4033000 nid=0x7a3a runnable "GC task thread#15 (ParallelGC)" prio=10 tid=0x00007f6da4035000 nid=0x7a3b runnable "GC task thread#16 (ParallelGC)" prio=10 tid=0x00007f6da4036800 nid=0x7a3c runnable "GC task thread#17 (ParallelGC)" prio=10 tid=0x00007f6da4038800 nid=0x7a3d runnable "VM Periodic Task Thread" prio=10 tid=0x00007f6da40dd000 nid=0x7a45 waiting on condition JNI global references: 886清單 6. jstack 運行輸出片段 2 WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 0 page no 843102 n bits 600 index `KEY_TSKTASK_MONTIME2` of table`dcnet_db/TSK_TASK` trx id 0 677833454 lock_mode X locks rec but not gap waiting Record lock, heap no 395 PHYSICAL RECORD: n_fields 3; compact format; info bits 0 0: len 8; hex 8000000000000425; asc %;; 1: len 8; hex 800012412c66d29c; asc A,f ;; 2: len 8; hex 800000000097629c; asc b ;;*** WE ROLL BACK TRANSACTION (1)我們假設(shè)涉事的數(shù)據(jù)表上面有一個索引,這次的死鎖就是由于兩條記錄同時訪問到了相同的索引造成的。
我們首先來看看 InnoDB 類型的數(shù)據(jù)表,只要能夠解決索引問題,就可以解決死鎖問題。MySQL 的 InnoDB 引擎是行級鎖,需要注意的是,這不是對記錄進行鎖定,而是對索引進行鎖定。在 UPDATE、DELETE 操作時,MySQL 不僅鎖定 WHERE 條件掃描過的所有索引記錄,而且會鎖定相鄰的鍵值,即所謂的 next-key locking;
如語句 UPDATE TSK_TASK SET UPDATE_TIME = NOW() WHERE ID > 10000 會鎖定所有主鍵大于等于 1000 的所有記錄,在該語句完成之前,你就不能對主鍵等于 10000 的記錄進行操作;當(dāng)非簇索引 (non-cluster index) 記錄被鎖定時,相關(guān)的簇索引 (cluster index) 記錄也需要被鎖定才能完成相應(yīng)的操作。
再分析一下發(fā)生問題的兩條 SQL 語句:
當(dāng)“update TSK_TASK set STATUS_ID=1064,UPDATE_TIME=now () where STATUS_ID=1061 and MON_TIME<date_sub(now(), INTERVAL 30 minute)”執(zhí)行時,MySQL 會使用 KEY_TSKTASK_MONTIME2 索引,因此首先鎖定相關(guān)的索引記錄,因為 KEY_TSKTASK_MONTIME2 是非簇索引,為執(zhí)行該語句,MySQL 還會鎖定簇索引(主鍵索引)。
假設(shè)“update TSK_TASK set STATUS_ID=1067,UPDATE_TIME=now () where ID in (9921180)”幾乎同時執(zhí)行時,本語句首先鎖定簇索引 (主鍵),由于需要更新 STATUS_ID 的值,所以還需要鎖定 KEY_TSKTASK_MONTIME2 的某些索引記錄。
這樣第一條語句鎖定了 KEY_TSKTASK_MONTIME2 的記錄,等待主鍵索引,而第二條語句則鎖定了主鍵索引記錄,而等待 KEY_TSKTASK_MONTIME2 的記錄,這樣死鎖就產(chǎn)生了。
我們通過拆分第一條語句解決了死鎖問題:即先查出符合條件的 ID:
select ID from TSK_TASK where STATUS_ID=1061 and MON_TIME < date_sub(now(), INTERVAL 30 minute);然后再更新狀態(tài):
update TSK_TASK set STATUS_ID=1064 where ID in (….);結(jié)束語
我們發(fā)現(xiàn),死鎖雖然是較早就被發(fā)現(xiàn)的問題,但是很多情況下我們設(shè)計的程序里還是經(jīng)常發(fā)生死鎖情況。我們不能只是分析如何解決死鎖這類問題,還需要具體找出預(yù)防死鎖的方法,這樣才能從根本上解決問題。總的來說,還是需要系統(tǒng)架構(gòu)師、程序員不斷積累經(jīng)驗,從業(yè)務(wù)邏輯設(shè)計層面徹底消除死鎖發(fā)生的可能性。
總結(jié)
以上是生活随笔為你收集整理的Java 程序死锁问题原理及解决方案的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 图书商城:订单模块
- 下一篇: React Native