一文詳解如何排查定位Java中的死鎖
一、服務(wù)死鎖,Linux 遇難題
在當(dāng)今數(shù)字化時(shí)代,微服務(wù)架構(gòu)憑借其高可擴(kuò)展性、靈活性和易于維護(hù)等優(yōu)勢,成為了眾多企業(yè)構(gòu)建大型應(yīng)用系統(tǒng)的首選架構(gòu)模式。當(dāng)我們將微服務(wù)部署在 Linux 服務(wù)器上時(shí),有時(shí)會(huì)遭遇令人頭疼的死鎖問題。死鎖一旦發(fā)生,就如同給微服務(wù)的運(yùn)行按下了 “暫停鍵”,會(huì)導(dǎo)致服務(wù)無法正常響應(yīng),嚴(yán)重影響系統(tǒng)的可用性和穩(wěn)定性,進(jìn)而對業(yè)務(wù)造成不良影響。
例如,在一個(gè)電商系統(tǒng)中,訂單微服務(wù)和庫存微服務(wù)可能會(huì)同時(shí)訪問共享的數(shù)據(jù)庫資源。如果訂單微服務(wù)在處理訂單時(shí)先獲取了訂單表的鎖,然后試圖獲取庫存表的鎖來更新庫存;而庫存微服務(wù)在處理庫存調(diào)整時(shí)先獲取了庫存表的鎖,接著又試圖獲取訂單表的鎖來關(guān)聯(lián)訂單信息。當(dāng)這兩個(gè)操作并發(fā)執(zhí)行時(shí),就有可能出現(xiàn)死鎖,導(dǎo)致訂單無法創(chuàng)建,庫存也無法更新,用戶在下單時(shí)會(huì)一直等待,嚴(yán)重影響購物體驗(yàn)。
面對這樣的困境,快速準(zhǔn)確地排查和解決死鎖問題顯得尤為重要。而 JPS(Java Virtual Machine Process Status Tool)和 Jstack(Java Stack Trace)命令,就像是兩把鋒利的 “寶劍”,為我們在 Linux 環(huán)境下排查微服務(wù)死鎖提供了有力的支持 。接下來,就讓我們深入了解如何使用這兩個(gè)命令來排查死鎖問題。
二、死鎖揭秘:原因與場景剖析
(一)死鎖形成原因
系統(tǒng)資源不足:系統(tǒng)中的資源是有限的,當(dāng)多個(gè)線程或進(jìn)程競爭這些有限的資源時(shí),如果資源的數(shù)量無法滿足所有線程或進(jìn)程的需求,就可能導(dǎo)致死鎖。例如,在一個(gè)多線程的數(shù)據(jù)庫應(yīng)用中,多個(gè)線程同時(shí)請求數(shù)據(jù)庫連接資源,如果數(shù)據(jù)庫連接池中的連接數(shù)量有限,當(dāng)所有連接都被占用時(shí),新的線程請求連接就會(huì)被阻塞,若這些線程在等待連接的同時(shí)又持有其他資源不釋放,就有可能引發(fā)死鎖。
進(jìn)程推進(jìn)順序不當(dāng):進(jìn)程在運(yùn)行過程中,請求和釋放資源的順序不合理,也會(huì)導(dǎo)致死鎖的發(fā)生。比如線程 A 先獲取了資源 X,然后嘗試獲取資源 Y;而線程 B 先獲取了資源 Y,接著嘗試獲取資源 X。如果這兩個(gè)線程并發(fā)執(zhí)行,就會(huì)出現(xiàn)相互等待的情況,從而產(chǎn)生死鎖。
資源分配不當(dāng):資源分配算法不合理或者資源分配過程中出現(xiàn)錯(cuò)誤,也可能引發(fā)死鎖。例如,在一個(gè)分布式系統(tǒng)中,不同節(jié)點(diǎn)上的進(jìn)程對共享資源的分配沒有進(jìn)行有效的協(xié)調(diào),導(dǎo)致某些進(jìn)程獲取了過多的資源,而其他進(jìn)程卻無法獲取到必要的資源,進(jìn)而引發(fā)死鎖。
(二)死鎖產(chǎn)生的必要條件
互斥條件:指資源在某一時(shí)刻只能被一個(gè)線程或進(jìn)程所使用,其他線程或進(jìn)程若要使用該資源,必須等待其被釋放。例如,打印機(jī)在打印任務(wù)時(shí),同一時(shí)間只能為一個(gè)進(jìn)程服務(wù),其他進(jìn)程需要等待打印機(jī)完成當(dāng)前任務(wù)后才能使用。
請求和保持條件:一個(gè)線程或進(jìn)程在請求新資源的同時(shí),會(huì)保持對已獲得資源的占有。例如,線程 A 已經(jīng)獲取了資源 X,在請求資源 Y 時(shí),它不會(huì)釋放資源 X,若資源 Y 被其他線程占用,線程 A 就會(huì)處于阻塞狀態(tài),但依然持有資源 X。
不剝奪條件:線程或進(jìn)程已獲得的資源,在未使用完之前,不能被其他線程或進(jìn)程強(qiáng)行剝奪,只能由持有該資源的線程或進(jìn)程自行釋放。比如,某個(gè)線程獲得了一個(gè)文件的寫鎖,在它完成寫操作并釋放鎖之前,其他線程無法強(qiáng)行獲取該寫鎖。
環(huán)路等待條件:多個(gè)線程或進(jìn)程之間形成一種頭尾相接的循環(huán)等待資源關(guān)系。例如,線程 A 等待線程 B 釋放資源 Y,線程 B 等待線程 C 釋放資源 Z,而線程 C 又等待線程 A 釋放資源 X,這樣就形成了一個(gè)循環(huán)等待的環(huán)路,導(dǎo)致死鎖。
(三)死鎖出現(xiàn)的場景
多個(gè)線程彼此申請對方資源:這是最常見的死鎖場景之一。假設(shè)有兩個(gè)線程 T1 和 T2,T1 持有資源 R1,然后試圖獲取 T2 持有的資源 R2;同時(shí),T2 持有資源 R2,并試圖獲取 T1 持有的資源 R1。由于雙方都在等待對方釋放自己所需的資源,從而陷入死鎖。例如,在一個(gè)圖形繪制程序中,線程 T1 負(fù)責(zé)繪制圖形的輪廓,持有畫筆資源 R1,在繪制填充顏色時(shí)需要獲取顏料資源 R2;而線程 T2 負(fù)責(zé)填充顏色,持有顏料資源 R2,在繪制輪廓時(shí)需要獲取畫筆資源 R1,若它們同時(shí)執(zhí)行,就可能出現(xiàn)死鎖。
單個(gè)線程申請新資源時(shí)產(chǎn)生死鎖:當(dāng)一個(gè)線程已經(jīng)持有一些資源,在申請新的資源時(shí),如果新資源被其他線程占用,而該線程又不釋放已持有的資源,就可能導(dǎo)致死鎖。比如,一個(gè)線程在處理事務(wù)時(shí),已經(jīng)獲取了數(shù)據(jù)庫的部分鎖,在需要獲取更多鎖來完成事務(wù)時(shí),由于其他線程持有這些鎖,該線程就會(huì)陷入等待,同時(shí)它又不釋放已持有的鎖,從而引發(fā)死鎖。
三、工具登場:JPS 與 Jstack 介紹
(一)JPS 命令詳解
JPS 命令是 Java Development Kit(JDK)提供的一個(gè)工具,主要用途是列出 JVM 進(jìn)程(Java 虛擬機(jī)進(jìn)程)的信息。在排查服務(wù)死鎖問題時(shí),它是我們獲取目標(biāo) Java 進(jìn)程 ID 的重要手段 。在開發(fā)和調(diào)試 Java 應(yīng)用程序時(shí),使用 JPS 命令可以顯示正在運(yùn)行的 Java 程序的進(jìn)程 ID(PID)以及其他相關(guān)信息,如程序的完整類名,即 Java 主類類名。
JPS 命令的基本語法為:jps [ options ] [ hostid ] ,其中 option 參數(shù)用于指定不同的選項(xiàng),hostid 參數(shù)用于指定要查詢的遠(yuǎn)程主機(jī)。如果不指定任何選項(xiàng),直接執(zhí)行 jps 命令,它會(huì)列出當(dāng)前系統(tǒng)中所有的 Java 進(jìn)程 ID 以及對應(yīng)的主類名。例如,在 Linux 系統(tǒng)中打開終端,進(jìn)入到項(xiàng)目所在目錄,執(zhí)行 jps 命令,可能會(huì)得到如下輸出:
12345 MainClass 12346 Jps
上述輸出中,12345 是運(yùn)行 MainClass 的 Java 進(jìn)程 ID,12346 是當(dāng)前執(zhí)行 jps 命令的進(jìn)程 ID。
常用的選項(xiàng)有:
-l:顯示完整的包名和應(yīng)用程序主類名。比如執(zhí)行 jps -l ,輸出可能為:
12345 com.example.demo.MainClass 12346 sun.tools.jps.Jps
這樣我們就能更清晰地看到 Java 進(jìn)程對應(yīng)的完整類名。
-m:顯示完整的包名、應(yīng)用程序主類名和虛擬機(jī)的啟動(dòng)參數(shù)。執(zhí)行 jps -m ,輸出示例如下:
12345 com.example.demo.MainClass --param1 value1 --param2 value2 12346 sun.tools.jps.Jps -Dapplication.home=/usr/local/jdk1.8.0_291 -Xms8m
通過這個(gè)選項(xiàng),我們可以了解 Java 進(jìn)程啟動(dòng)時(shí)傳入的參數(shù)。
-v:顯示虛擬機(jī)的啟動(dòng)參數(shù)和 JVM 命令行選項(xiàng)。執(zhí)行 jps -v ,輸出可能是:
12345 com.example.demo.MainClass -Xmx512m -Xms256m -XX:MaxPermSize=256m 12346 sun.tools.jps.Jps -Dapplication.home=/usr/local/jdk1.8.0_291 -Xms8m
這有助于我們查看 Java 進(jìn)程的 JVM 配置參數(shù)。
-q:只顯示進(jìn)程 ID,不顯示類名和主類名。執(zhí)行 jps -q ,輸出結(jié)果類似:
12345 12346
這種方式在只需要獲取進(jìn)程 ID 時(shí)非常簡潔高效。
(二)Jstack 命令詳解
Jstack 是 Java 虛擬機(jī)自帶的一種堆棧跟蹤工具,它的主要用途是生成 Java 虛擬機(jī)當(dāng)前時(shí)刻的線程快照。線程快照是當(dāng)前 Java 虛擬機(jī)內(nèi)每一條線程正在執(zhí)行的方法堆棧的集合,通過分析這個(gè)快照,我們可以定位線程出現(xiàn)長時(shí)間停頓的原因,比如線程間死鎖、死循環(huán)、請求外部資源導(dǎo)致的長時(shí)間等待等。在排查微服務(wù)死鎖問題時(shí),Jstack 命令起著關(guān)鍵作用,它能夠幫助我們深入了解線程的運(yùn)行狀態(tài)和方法調(diào)用情況,從而找出死鎖的根源。
Jstack 命令的基本語法為:jstack [ options ] pid ,其中 options 是可選參數(shù),pid 是要分析的 Java 進(jìn)程 ID。常用的選項(xiàng)有:
- -l:Long listing,會(huì)打印出額外的鎖信息,在發(fā)生死鎖時(shí)可以用 jstack -l pid 來觀察鎖持有情況。當(dāng)我們懷疑微服務(wù)出現(xiàn)死鎖時(shí),使用這個(gè)選項(xiàng)可以獲取更詳細(xì)的鎖相關(guān)信息,例如:
jstack -l 12345
執(zhí)行上述命令后,輸出結(jié)果中會(huì)包含每個(gè)線程持有的鎖以及等待獲取的鎖的詳細(xì)信息,這對于判斷是否存在死鎖以及死鎖的具體情況非常有幫助。
- -m:mixed mode,不僅會(huì)輸出 Java 堆棧信息,還會(huì)輸出 C/C++ 堆棧信息(比如 Native 方法)。如果 Java 程序中調(diào)用了本地方法,使用這個(gè)選項(xiàng)可以查看本地方法的堆棧信息,有助于全面分析問題,命令示例如下:
jstack -m 12345
- -F:Force a stack dump when jstack [-l] pid does not respond。當(dāng)正常的請求不被響應(yīng)時(shí),強(qiáng)制輸出堆棧信息。在某些情況下,目標(biāo) Java 進(jìn)程可能處于無響應(yīng)狀態(tài),此時(shí)使用 -F 選項(xiàng)可以強(qiáng)制獲取線程堆棧信息,例如:
jstack -F 12345
通過 Jstack 命令獲取到的線程堆棧信息中,包含了豐富的內(nèi)容,如線程的狀態(tài)(RUNNABLE、BLOCKED、WAITING 等)、線程正在執(zhí)行的方法、方法的調(diào)用棧以及鎖的持有和等待情況等。這些信息對于我們排查死鎖問題至關(guān)重要,能夠幫助我們準(zhǔn)確地定位到死鎖發(fā)生的位置和原因。
四、實(shí)戰(zhàn)演練:排查死鎖步驟
(一)復(fù)現(xiàn)死鎖:Java 代碼示例
下面是一段導(dǎo)致死鎖的 Java 代碼示例,通過這段代碼可以清晰地看到線程是如何競爭資源并最終導(dǎo)致死鎖的。
public class DeadLockExample { private static final Object lock1 = new Object(); private static final Object lock2 = new Object(); public static void main(String[] args) { Thread thread1 = new Thread(() -> { synchronized (lock1) { System.out.println("Thread 1: Holding lock1"); try { Thread.sleep(1000); // 讓線程1持有l(wèi)ock1一段時(shí)間,確保線程2有機(jī)會(huì)獲取lock2 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Thread 1: Waiting for lock2"); synchronized (lock2) { System.out.println("Thread 1: Holding lock1 and lock2"); } } }); Thread thread2 = new Thread(() -> { synchronized (lock2) { System.out.println("Thread 2: Holding lock2"); try { Thread.sleep(1000); // 讓線程2持有l(wèi)ock2一段時(shí)間,確保線程1有機(jī)會(huì)獲取lock1 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Thread 2: Waiting for lock1"); synchronized (lock1) { System.out.println("Thread 2: Holding lock1 and lock2"); } } }); thread1.start(); thread2.start(); } }
在這段代碼中,thread1 首先獲取了 lock1 ,然后睡眠 1 秒,這期間 thread2 有機(jī)會(huì)獲取 lock2 。接著,thread1 試圖獲取 lock2 ,而 thread2 試圖獲取 lock1 ,由于雙方都持有對方需要的資源且不釋放,從而形成了死鎖。
(二)使用 JPS 查找進(jìn)程 ID
將上述 Java 代碼打包成一個(gè)可執(zhí)行的 JAR 文件,然后部署到 Linux 服務(wù)器上運(yùn)行。在 Linux 終端中,使用 jps 命令來查找正在運(yùn)行的 Java 進(jìn)程 ID。假設(shè)我們將這個(gè) JAR 文件命名為 deadlock-demo.jar ,運(yùn)行命令如下:
java -jar deadlock-demo.jar
運(yùn)行后,打開新的終端,執(zhí)行 jps 命令:
jps
輸出結(jié)果可能如下:
12345 DeadLockExample 12346 Jps
這里的 12345 就是運(yùn)行 DeadLockExample 類的 Java 進(jìn)程 ID,我們后續(xù)排查死鎖就需要用到這個(gè) ID。
(三)使用 Jstack 分析線程堆棧
得到 Java 進(jìn)程 ID 后,使用 jstack 命令來分析線程堆棧信息,從而定位死鎖。執(zhí)行命令如下:
jstack -l 12345
其中,-l 選項(xiàng)表示輸出額外的鎖信息,這對于分析死鎖非常有幫助。命令執(zhí)行后,會(huì)輸出大量的線程堆棧信息,我們重點(diǎn)關(guān)注與死鎖相關(guān)的部分。以下是可能的輸出結(jié)果(為了突出重點(diǎn),進(jìn)行了簡化):
Found one Java-level deadlock: ============================= "Thread-1": waiting to lock monitor 0x00007f85a8003ae8 (object 0x00000007d6aa2c98, a java.lang.Object), which is held by "Thread-0" "Thread-0": waiting to lock monitor 0x00007f85a8006168 (object 0x00000007d6aa2ca8, a java.lang.Object), which is held by "Thread-1" Java stack information for the threads listed above: =================================================== "Thread-1": at DeadLockExample.lambda$main$1(DeadLockExample.java:22) - waiting to lock <0x00000007d6aa2c98> (a java.lang.Object) - locked <0x00000007d6aa2ca8> (a java.lang.Object) at java.lang.Thread.run(Thread.java:748) "Thread-0": at DeadLockExample.lambda$main$0(DeadLockExample.java:12) - waiting to lock <0x00000007d6aa2ca8> (a java.lang.Object) - locked <0x00000007d6aa2c98> (a java.lang.Object) at java.lang.Thread.run(Thread.java:748) Found 1 deadlock.
從輸出結(jié)果中可以看到,Thread-1 正在等待獲取 Thread-0 持有的鎖(0x00000007d6aa2c98 ),而 Thread-0 正在等待獲取 Thread-1 持有的鎖(0x00000007d6aa2ca8 ),這就形成了死鎖。同時(shí),還可以看到死鎖發(fā)生的具體代碼行,如 DeadLockExample.java:22 和 DeadLockExample.java:12 ,這為我們進(jìn)一步排查和解決死鎖問題提供了關(guān)鍵線索。
五、總結(jié)與展望
在本次死鎖排查過程中,我們首先通過一個(gè)簡單的 Java 代碼示例復(fù)現(xiàn)了死鎖問題,然后借助 JPS 命令快速準(zhǔn)確地獲取到了目標(biāo) Java 進(jìn)程 ID,為后續(xù)的分析工作奠定了基礎(chǔ)。接著,使用 Jstack 命令對線程堆棧進(jìn)行分析,成功找到了死鎖的關(guān)鍵信息,包括死鎖的線程、持有的鎖以及等待獲取的鎖等,從而清晰地定位到了死鎖的根源。
死鎖問題對系統(tǒng)的正常運(yùn)行危害極大,它不僅會(huì)導(dǎo)致服務(wù)中斷,影響用戶體驗(yàn),還可能造成資源的浪費(fèi)和系統(tǒng)性能的下降。因此,在開發(fā)和部署服務(wù)時(shí),避免死鎖的發(fā)生至關(guān)重要。為了預(yù)防死鎖,我們可以采取多種措施。在代碼編寫階段,要確保所有線程以相同的順序獲取鎖,避免嵌套鎖的使用,減少鎖的持有時(shí)間。在資源分配方面,合理規(guī)劃資源的使用和分配,避免資源的過度競爭和不合理分配。同時(shí),可以使用一些高級的同步工具,如 ReentrantLock、Semaphore 等,它們提供了更靈活的同步控制,有助于降低死鎖發(fā)生的風(fēng)險(xiǎn)。此外,定期對系統(tǒng)進(jìn)行性能測試和死鎖檢測,及時(shí)發(fā)現(xiàn)并解決潛在的死鎖問題也是非常必要的。
隨著架構(gòu)的不斷發(fā)展和應(yīng)用場景的日益復(fù)雜,死鎖問題可能會(huì)以更加隱蔽和復(fù)雜的形式出現(xiàn)。未來,我們需要不斷探索和研究新的死鎖檢測和預(yù)防技術(shù),結(jié)合人工智能、大數(shù)據(jù)分析等新興技術(shù),實(shí)現(xiàn)對死鎖問題的智能預(yù)測和自動(dòng)處理,進(jìn)一步提升微服務(wù)系統(tǒng)的穩(wěn)定性和可靠性。
以上就是一文詳解如何排查定位Java中的死鎖的詳細(xì)內(nèi)容,更多關(guān)于排查定位Java死鎖的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
詳解Java中NullPointerException異常的原因和解決辦法
本文主要介紹了詳解Java中NullPointerException異常的原因和解決辦法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-07-07JAVA8妙用Optional解決判斷Null為空的問題方法
本文主要介紹了JAVA8妙用Optional解決判斷Null為空的問題方法,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-10-10IDEA創(chuàng)建Servlet編寫HelloWorldServlet頁面詳細(xì)教程(圖文并茂)
在學(xué)習(xí)servlet過程中參考的教程是用eclipse完成的,而我在練習(xí)的過程中是使用IDEA的,在創(chuàng)建servlet程序時(shí)遇到了挺多困難,在此記錄一下,這篇文章主要給大家介紹了關(guān)于IDEA創(chuàng)建Servlet編寫HelloWorldServlet頁面詳細(xì)教程的相關(guān)資料,需要的朋友可以參考下2023-10-10springboot3整合遠(yuǎn)程調(diào)用的過程解析
遠(yuǎn)程過程調(diào)用主要分為:服務(wù)提供者,服務(wù)消費(fèi)者,通過連接對方服務(wù)器進(jìn)行請求交互,來實(shí)現(xiàn)調(diào)用效果,這篇文章主要介紹了springboot3整合遠(yuǎn)程調(diào)用,需要的朋友可以參考下2023-06-06Java從網(wǎng)絡(luò)讀取圖片并保存至本地實(shí)例
這篇文章主要為大家詳細(xì)介紹了Java從網(wǎng)絡(luò)讀取圖片并保存至本地的實(shí)例,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-04-04Spring Boot 項(xiàng)目啟動(dòng)失敗的解決方案
這篇文章主要介紹了Spring Boot 項(xiàng)目啟動(dòng)失敗的解決方案,幫助大家更好的理解和學(xué)習(xí)使用Spring Boot,感興趣的朋友可以了解下2021-03-03SpringBoot實(shí)現(xiàn)WebSocket即時(shí)通訊的示例代碼
本文主要介紹了SpringBoot實(shí)現(xiàn)WebSocket即時(shí)通訊的示例代碼,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-04-04