假笨说-类初始化死锁导致线程被打爆!打爆!爆!
概述
之前寫過關(guān)于類加載死鎖的文章,消失的死鎖,說的是類加載過程中發(fā)生的死鎖,我們從線程dump里完全看不出死鎖的跡象,但是確實發(fā)生了死鎖,沒了解的建議看看我前面的那篇文章
本文要說的是另外一個問題,最近在生產(chǎn)環(huán)境上碰到,是類初始化導(dǎo)致的死鎖,恩,你沒看錯,確實是類初始化導(dǎo)致的死鎖,我之前寫過一篇文章,不可逆的類初始化過程,這篇文章可以助你了解類的初始化過程,另外也寫過一篇JDK的sql設(shè)計不合理導(dǎo)致的驅(qū)動類初始化死鎖問題,也是關(guān)于初始化死鎖的,原因其實差不多,不過本文將這個問題描述的場景更加通用化了
我們線上的現(xiàn)象是發(fā)現(xiàn)非常多的線程都卡死在同一個地方,也不是在做類加載,如果是死循環(huán),那cpu肯定上去了,但是cpu并沒有上去,因此比較詭異
PS:有人經(jīng)常給我公眾號發(fā)消息咨詢問題,可消息最多只能保存最近5天的,而且只能回復(fù)最近2天的,有時候忘記回了想起要回的時候就不能再回復(fù)了,如果比較緊急,問題可以發(fā)到我郵箱里,我會抽時間看這些問題并回答,不過無法保證所有的問題都會回答,因為問的人確實有點多,精力也有限。。。
Demo
嚴(yán)格意義上說,這個Demo里提到的情況是其中一個簡單的場景,和我們線上碰到的場景會有點出入,比這個會更復(fù)雜點,我后面也會提到那個場景
為了讓問題能重現(xiàn),我選擇了一個最簡單的辦法,就是debug,一般情況下,并發(fā)導(dǎo)致的問題,通過debug都可以模擬出來,并發(fā)無非就是控制代碼執(zhí)行的先后順序,debug顯然可以做到這一點
我們上面定義了A,B兩個類,他們相互依賴,并且都有一個靜態(tài)塊,在靜態(tài)塊里相互調(diào)用對方的某個靜態(tài)方法,我們的測試類ABTest就是用兩個線程分別取調(diào)用兩個類的靜態(tài)方法,那我們在A和B兩個類的靜態(tài)塊里調(diào)用對方靜態(tài)方法之前設(shè)置一個斷點,比如說都在System.out.println()那里設(shè)置斷點,當(dāng)兩個線程都停到斷點處的時候,我們再過掉兩個斷點,你會發(fā)現(xiàn)一個奇怪的現(xiàn)象,這個進(jìn)程并沒有退出,也就是那兩個線程都沒有執(zhí)行完,你看到堆棧如下:
這里你看下Thread狀態(tài)是RUNNABLE,但是又是卡在Object.wait()處的,這里確實只能說是JVM里的一個bug吧,狀態(tài)不一致,我之前在InfoQ上發(fā)過一篇文章JVM Bug:多個線程持有一把鎖,解釋了這個狀態(tài)不一致的問題。
Object.wait是哪里調(diào)的
從線程dump的線程棧來看完全看不出是調(diào)用了Object.wait,但是從線程輸出來看確實有Object.wait,為了找出哪里調(diào)用了它,我們可以通過jstack -m <pid>來看,看到輸出之后,你會覺得不可思議,確實有wait的邏輯
那這個邏輯從名字上來不難猜到是正在做類的初始化,那我們先來了解下類的初始化過程
類的初始化過程
當(dāng)我們第一次主動調(diào)用某個類的靜態(tài)方法就會觸發(fā)這個類的初始化,當(dāng)然還有其他的觸發(fā)情況,類的初始化說白了就是在類加載起來之后,在某個合適的時機執(zhí)行這個類的clinit方法,clinit方法是什么?比如我們在類里聲明一段static代碼塊,或者有靜態(tài)屬性,javac會將這些代碼都統(tǒng)一放到一個叫做clinit的方法里,在類初始化的時候來執(zhí)行這個方法,但是JVM必須要保證這個方法只能被執(zhí)行一次,如果有其他線程并發(fā)調(diào)用觸發(fā)了這個類的多次初始化,那只能讓一個線程真正執(zhí)行clinit方法,其他線程都必須等待,當(dāng)clinit方法執(zhí)行完之后,然后再喚醒其他等待這里的線程繼續(xù)操作,當(dāng)然不會再讓它們有機會再執(zhí)行clinit方法,因為每個類都有一個狀態(tài),這個狀態(tài)可以保證這一點
當(dāng)有個線程正在執(zhí)行這個類的clinit方法的時候,就會設(shè)置這個類的狀態(tài)為being_initialized,當(dāng)正常執(zhí)行完之后就馬上設(shè)置為fully_initialized,然后才喚醒其他也在等著對其做初始化的線程繼續(xù)往下走,在繼續(xù)走下去之前,會先判斷這個類的狀態(tài),如果已經(jīng)是fully_initialized了說明有線程已經(jīng)執(zhí)行完了clinit方法,因此不會再執(zhí)行clinit方法了
當(dāng)然如果執(zhí)行clinit失敗了,那我之前那篇不可逆的類初始化過程文章就著重講了這種情況,可以去看看。
看到這里是否能解釋了我們線上為什么會有那么多線程會卡在某一個地方了?因為這個類的狀態(tài)是being_initialized,所以只能等啦
Demo現(xiàn)象解釋
我們Demo里的那兩個線程,從dump來看確實是死鎖了,那這個場景當(dāng)時是怎么發(fā)生的呢?線程1首先執(zhí)行B.test(),于是會對B類做初始化,設(shè)置B的類狀態(tài)為being_initialized,接著去執(zhí)行B的clinit方法,但是在clinit方法里要去調(diào)用A.test方法,理論上此時會對A做初始化并調(diào)用其test方法,但是就在設(shè)置完B的類狀態(tài)之后,執(zhí)行其clinit里的A.test方法之前,線程2卻執(zhí)行了A.test方法,此時線程2會優(yōu)先負(fù)責(zé)對A的初始化工作,即設(shè)置A類的狀態(tài)為being_initialized,然后再去執(zhí)行A的clinit方法,此時線程1發(fā)現(xiàn)A的類狀態(tài)是being_initialized了,那線程1就認(rèn)為有線程對A類正在做初始化,于是就等待了,而線程2同樣發(fā)現(xiàn)B的類狀態(tài)也是being_initialized,于是也開始等待,這樣就形成了互等的情況,造成了類死鎖的現(xiàn)象。
更隱蔽的初始化死鎖現(xiàn)象
這里提到的場景其實是我們線上的場景,這個情況不是很好模擬,比較難控制,當(dāng)然debug jvm還是可以的
上述代碼不一定能重現(xiàn),不過我可以跟大家解釋下可能死鎖的情況,代碼里我們主要定義了
-
Iterator接口:這個接口里有個static屬性,static方法,還有個default方法,這意味著這個Iterator接口有個clinit方法,里面主要是對這個static屬性賦值
-
AbstractIterator抽象類:沒啥東西,就是實現(xiàn)Iterator接口罷了
-
Test測試類:起了兩個線程,分別new了一個AbstractIterator匿名子類實例以及調(diào)用Iterator的靜態(tài)方法
ok,到此我要描述一個特殊的場景了,線程1執(zhí)行會創(chuàng)建一個AbstractIterator匿名子類實例,此時會觸發(fā)AbstractIterator的初始化,同時因為其實現(xiàn)了Iterator接口,而Iterator接口含有defalut方法,因此這個類會被標(biāo)記是一個含有default方法的類,于是在設(shè)置完AbstractIterator的類狀態(tài)為being_initialized之后,會遞歸遍歷其父接口,如果某個接口有default方法,比如Iterator,那就先觸發(fā)Iterator類的初始化動作,但是在觸發(fā)這個動作之前,線程2執(zhí)行Iterator.empty靜態(tài)方法了,于是會觸發(fā)對Iterator類的初始化動作,于是設(shè)置Iterator的類狀態(tài)為being_initialized,然后開始執(zhí)行其clinit方法,而在clinit方法里有創(chuàng)建AbstractIterator匿名子類的實例,于是就會想觸發(fā)AbstractIterator的初始化,但是AbstractIterator已經(jīng)被線程1設(shè)置為being_initialized了,于是就只能等了,同理,線程1因為要等Iterator的初始化完成而必須等待了,從而互鎖現(xiàn)象再次形成
相比我們最早Demo里的場景最大的不同是我們看線程棧,只能看到一個線程在執(zhí)行clinit方法,另外一個線程并還沒有在支持clinit方法,因此這個線程卡在了初始化其父接口初始化的路上了,還沒拿到執(zhí)行clinit的機會。
總結(jié)
類加載的死鎖很隱蔽了,但是類初始化的死鎖更隱蔽,所以大家要謹(jǐn)記在類的初始化代碼里產(chǎn)生循環(huán)依賴,另外對于jdk8的defalut特性也要謹(jǐn)慎,因為這會直接觸發(fā)接口的初始化導(dǎo)致更隱蔽的循環(huán)依賴。
總結(jié)
以上是生活随笔為你收集整理的假笨说-类初始化死锁导致线程被打爆!打爆!爆!的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux 小白启航之路-搭建linux
- 下一篇: node.js