講講JAVA中的死鎖問題

引言

今天在網上看到了一個圖片,嗯,似乎給自己的未來找到了方向:

640?wx_fmt=jpeg

大家要努力!
嗯,開始我們的正題。今天我們來講講java中的死鎖問題,大致分爲下面三個小點
  • 如何檢測死鎖

  • 如何預防死鎖

  • 隱蔽的死鎖

正文

如何檢測死鎖

首先,我們先明白在什麼情況下會懷疑是死鎖?
簡單,就是程序沒有響應的時候。其實排查步驟和《談談線上CPU100%排查套路》是類似的。但是有一個區別,在死鎖的情況下,cpu不會跑滿。
也就是說,當你發現程序沒有響應,cpu佔用率又不是特別高的時候,第一反應就是應該是死鎖
那麼,死鎖發生的情形也很簡單,如下圖所示,

640?wx_fmt=png

我們準備一段程序,模擬一下

640?wx_fmt=png


排查步驟也很簡單,執行下面的jps命令看看當前執行任務的進程號
D:\Java>jps
22320 Launcher
27488
25612 Jps
6700 TestDeadLock

然後用jstack命令打印出6700這個進程中,線程的信息

D:\Java>jstack 6700

輸出結果中,有這麼一段,就能看到死鎖線程信息

640?wx_fmt=png

多嘴一句,在輸出結果中你能看到線程的各種狀態,如下所示

640?wx_fmt=png

Thread的源碼中有一個State枚舉類定義了這些線程狀態,轉換關係如下(圖片出自網絡)

640?wx_fmt=png

初始狀態
實現Runnable接口和繼承Thread可以得到一個線程類,new一個實例出來,線程就進入了初始狀態。
就緒狀態
就緒狀態只是說你資格運行,調度程序沒有挑選到你,你就永遠是就緒狀態。
調用線程的start()方法,此線程進入就緒狀態。
當前線程sleep()方法結束,其他線程join()結束,等待用戶輸入完畢,某個線程拿到對象鎖,這些線程也將進入就緒狀態。
當前線程時間片用完了,調用當前線程的yield()方法,當前線程進入就緒狀態。
鎖池裏的線程拿到對象鎖後,進入就緒狀態。
運行中狀態
線程調度程序從可運行池中選擇一個線程作爲當前線程時線程所處的狀態。這也是線程進入運行狀態的唯一一種方式。
阻塞狀態
阻塞狀態是線程阻塞在進入synchronized關鍵字修飾的方法或代碼塊(獲取鎖)時的狀態。
等待
處於這種狀態的線程不會被分配CPU執行時間,它們要等待被顯式地喚醒,否則會處於無限期等待的狀態。
超時等待
處於這種狀態的線程不會被分配CPU執行時間,不過無須無限期等待被其他線程顯示地喚醒,在達到一定時間後它們會自動喚醒。
終止狀態
當線程的run()方法完成時,或者主線程的main()方法完成時,我們就認爲它終止了。這個線程對象也許是活的,但是,它已經不是一個單獨執行的線程。線程一旦終止了,就不能復生。
在一個終止的線程上調用start()方法,會拋出java.lang.IllegalThreadStateException異常。

如何預防死鎖

儘量保證加鎖順序是一樣的
例如有A,B,C三把鎖。
Thread 1的加鎖順序爲A、B、C這樣的。
Thread 2的加鎖順序爲A、C
這樣就不會死鎖。
如果Thread2的加鎖順序爲B、A或者C、A這樣順序就不一致了,就會出現死鎖問題。
儘量用超時放棄機制
Lock接口提供了tryLock(long time, TimeUnit unit)方法,該方法可以按照固定時長等待鎖,因此線程可以在獲取鎖超時以後,主動釋放之前已經獲得的所有的鎖。可以避免死鎖問題

隱蔽的死鎖

因爲我在生產上碰到過幾次死鎖,線程日誌裏是看不到死鎖信息的。一般這種情況下都是直接看線程棧的內容,自己分析。
例如下面這個併發加載類導致的死鎖

640?wx_fmt=png

640?wx_fmt=png

640?wx_fmt=png

代碼很簡單,準備兩個線程並行加載,在A類中加載B類。而在B類中加載A類。就會導致死鎖,此時按上述步驟是看不到死鎖信息的。你只能看到下面這樣的信息

640?wx_fmt=png

線程都是處在Runable狀態的,看不到死鎖信息。
原因也很簡單:
static代碼裏頭的內容會被編譯放到clinit方法裏。JVM執行clinit時會給clinit加上鎖,防止多線程併發執行。兩個線程在初始化的時候,先各自加上clinit鎖。然後在通過Class.forName去加載對方,因此都想獲取對方要執行clinit的鎖,因此死鎖就此發生。

總結

本文講了java中死鎖相關知識,希望大家有所收穫!



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章