本文來自: PerfMa技術社區PerfMa(笨馬網絡)官網java
以前寫過關於類加載死鎖的文章,消失的死鎖,說的是類加載過程當中發生的死鎖,咱們從線程dump裏徹底看不出死鎖的跡象,可是確實發生了死鎖,沒了解的建議看看我前面的那篇文章sql
本文要說的是另一個問題,最近在生產環境上碰到,是類初始化致使的死鎖,恩,你沒看錯,確實是類初始化致使的死鎖,我以前寫過一篇文章,不可逆的類初始化過程,這篇文章能夠助你瞭解類的初始化過程,另外也寫過一篇JDK的sql設計不合理致使的驅動類初始化死鎖問題,也是關於初始化死鎖的,緣由其實差很少,不過本文將這個問題描述的場景更加通用化了網絡
咱們線上的現象是發現很是多的線程都卡死在同一個地方,也不是在作類加載,若是是死循環,那cpu確定上去了,可是cpu並無上去,所以比較詭異多線程
PS:有人常常給我公衆號發消息諮詢問題,可消息最多隻能保存最近5天的,並且只能回覆最近2天的,有時候忘記回了想起要回的時候就不能再回復了,若是比較緊急,問題能夠發到我郵箱裏,我會抽時間看這些問題並回答,不過沒法保證全部的問題都會回答,由於問的人確實有點多,精力也有限。。。併發
嚴格意義上說,這個Demo裏提到的狀況是其中一個簡單的場景,和咱們線上碰到的場景會有點出入,比這個會更復雜點,我後面也會提到那個場景jvm
爲了讓問題能重現,我選擇了一個最簡單的辦法,就是debug,通常狀況下,併發致使的問題,經過debug均可以模擬出來,併發無非就是控制代碼執行的前後順序,debug顯然能夠作到這一點學習
咱們上面定義了A,B兩個類,他們相互依賴,而且都有一個靜態塊,在靜態塊裏相互調用對方的某個靜態方法,咱們的測試類ABTest就是用兩個線程分別取調用兩個類的靜態方法,那咱們在A和B兩個類的靜態塊裏調用對方靜態方法以前設置一個斷點,好比說都在System.out.println()
那裏設置斷點,當兩個線程都停到斷點處的時候,咱們再過掉兩個斷點,你會發現一個奇怪的現象,這個進程並無退出,也就是那兩個線程都沒有執行完,你看到堆棧以下:測試
這裏你看下Thread狀態是RUNNABLE,可是又是卡在Object.wait()
處的,這裏確實只能說是JVM裏的一個bug吧,狀態不一致。優化
從線程dump的線程棧來看徹底看不出是調用了Object.wait,可是從線程輸出來看確實有Object.wait,爲了找出哪裏調用了它,咱們能夠經過jstack -m <pid>
來看,看到輸出以後,你會以爲難以想象,確實有wait的邏輯spa
那這個邏輯從名字上來不難猜到是正在作類的初始化,那咱們先來了解下類的初始化過程
當咱們第一次主動調用某個類的靜態方法就會觸發這個類的初始化,固然還有其餘的觸發狀況,類的初始化說白了就是在類加載起來以後,在某個合適的時機執行這個類的clinit方法,clinit方法是什麼?好比咱們在類裏聲明一段static代碼塊,或者有靜態屬性,javac會將這些代碼都統一放到一個叫作clinit的方法裏,在類初始化的時候來執行這個方法,可是JVM必需要保證這個方法只能被執行一次,若是有其餘線程併發調用觸發了這個類的屢次初始化,那隻能讓一個線程真正執行clinit方法,其餘線程都必須等待,當clinit方法執行完以後,而後再喚醒其餘等待這裏的線程繼續操做,固然不會再讓它們有機會再執行clinit方法,由於每一個類都有一個狀態,這個狀態能夠保證這一點。
當有個線程正在執行這個類的clinit方法的時候,就會設置這個類的狀態爲being_initialized
,當正常執行完以後就立刻設置爲fully_initialized
,而後才喚醒其餘也在等着對其作初始化的線程繼續往下走,在繼續走下去以前,會先判斷這個類的狀態,若是已是fully_initialized
了說明有線程已經執行完了clinit方法,所以不會再執行clinit方法了。
固然若是執行clinit失敗了,那我以前那篇不可逆的類初始化過程文章就着重講了這種狀況,能夠去看看。
看到這裏是否能解釋了咱們線上爲何會有那麼多線程會卡在某一個地方了?由於這個類的狀態是being_initialized
,因此只能等啦
咱們Demo裏的那兩個線程,從dump來看確實是死鎖了,那這個場景當時是怎麼發生的呢?線程1首先執行B.test(),因而會對B類作初始化,設置B的類狀態爲being_initialized
,接着去執行B的clinit方法,可是在clinit方法裏要去調用A.test方法,理論上此時會對A作初始化並調用其test方法,可是就在設置完B的類狀態以後,執行其clinit裏的A.test方法以前,線程2卻執行了A.test方法,此時線程2會優先負責對A的初始化工做,即設置A類的狀態爲being_initialized
,而後再去執行A的clinit方法,此時線程1發現A的類狀態是being_initialized
了,那線程1就認爲有線程對A類正在作初始化,因而就等待了,而線程2一樣發現B的類狀態也是being_initialized
,因而也開始等待,這樣就造成了互等的狀況,形成了類死鎖的現象。
這裏提到的場景實際上是咱們線上的場景,這個狀況不是很好模擬,比較難控制,固然debug jvm仍是能夠的
上述代碼不必定能重現,不過我能夠跟你們解釋下可能死鎖的狀況,代碼裏咱們主要定義了
ok,到此我要描述一個特殊的場景了,線程1執行會建立一個AbstractIterator匿名子類實例,此時會觸發AbstractIterator的初始化,同時由於其實現了Iterator接口,而Iterator接口含有defalut方法,所以這個類會被標記是一個含有default方法的類,因而在設置完AbstractIterator的類狀態爲being_initialized
以後,會遞歸遍歷其父接口,若是某個接口有default方法,好比Iterator,那就先觸發Iterator類的初始化動做,可是在觸發這個動做以前,線程2執行Iterator.empty靜態方法了,因而會觸發對Iterator類的初始化動做,因而設置Iterator的類狀態爲being_initialized
,而後開始執行其clinit方法,而在clinit方法裏有建立AbstractIterator匿名子類的實例,因而就會想觸發AbstractIterator的初始化,可是AbstractIterator已經被線程1設置爲being_initialized
了,因而就只能等了,同理,線程1由於要等Iterator的初始化完成而必須等待了,從而互鎖現象再次造成
相比咱們最先Demo裏的場景最大的不一樣是咱們看線程棧,只能看到一個線程在執行clinit方法,另一個線程並尚未在支持clinit方法,所以這個線程卡在了初始化其父接口初始化的路上了,還沒拿到執行clinit的機會。
類加載的死鎖很隱蔽了,可是類初始化的死鎖更隱蔽,因此你們要謹記在類的初始化代碼裏產生循環依賴,另外對於jdk8的defalut特性也要謹慎,由於這會直接觸發接口的初始化致使更隱蔽的循環依賴。
一塊兒來學習吧: