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