類初始化造成的死鎖

1.死鎖是怎么產(chǎn)生的

類初始化是一個很隱蔽的操作,是由虛擬機(jī)主導(dǎo)完成的,開發(fā)人員不了解類加載機(jī)制的話,可能壓根不知道類初始化是個什么東東。類初始化的文章有專門講過,可參考Java虛擬機(jī)類加載機(jī)制,里面有詳細(xì)描述。
關(guān)于類初始化有幾個關(guān)鍵特性:

  • 類初始化的過程其實就是執(zhí)行類構(gòu)造器方法<clinit>()的過程;
  • 在子類初始化完成時,虛擬機(jī)會保證其父類有初始化完成;
  • 多線程環(huán)境下,虛擬機(jī)執(zhí)行<clinit>()方法會自動加鎖;

在java中,死鎖肯定是在多線程環(huán)境下產(chǎn)生的。多個線程同時需要互相持有的某個資源,自己的資源無法釋放,別人的資源又無法得到,造成循環(huán)依賴,進(jìn)而一直阻塞在那里,這樣就形成死鎖了。

2.產(chǎn)生死鎖的情況

2.1 兩個類初始化互相依賴

最明顯的情況是,2個類在不同的線程中初始化,彼此互相依賴,我們來看個例子:

public class Test { 

    public static class A {

        static {
            System.out.println("class A init.");
            B b = new B();
        }   
        
        public static void test() {
            System.out.println("method test called in class A");
        }
    }
    
    public static class B {
        
        static {
            System.out.println("class B init.");
            A a = new A();
        }
        
        public static void test() {
            System.out.println("method test called in class B");
        }
    }
    
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                A.test();
            }       
        }).start();
            
        new Thread(new Runnable() {
            @Override
            public void run() {
                B.test();
            }       
        }).start();
    }   
}

運行結(jié)果如下:

class A init.
class B init.

第一個線程執(zhí)行A.test()的時候,開始初始化類A,該線程獲得A.class的鎖,第二個線程執(zhí)行B.test()的時候,開始初始化類B,該線程獲得B.class的鎖。當(dāng)A在初始化過程中執(zhí)行代碼B b = new B()的時候,發(fā)現(xiàn)類B還沒有初始化完成,于是嘗試獲得類B.class的鎖;類B在初始化時執(zhí)行代碼A a = new A(),發(fā)現(xiàn)類A也沒有初始化完成,于是嘗試獲得類A.class的鎖,但A.class鎖已被占用,所以該線程會阻塞住,并等待該鎖的釋放;同樣第一個線程阻塞住并等待B.class鎖的釋放,這樣就造成循環(huán)依賴,形成了死鎖。

如果把上面代碼改為如下執(zhí)行方式,會出現(xiàn)什么結(jié)果呢?

public static void main(String[] args) {
    A.test();
    B.test();
}

乍一看去,好像A初始化時依賴B,B初始化時依賴A,也會造成死鎖,但實際上并不會。A、B兩個類的初始化都是在同一個線程里執(zhí)行的,初始化A的時候,該線程會獲得A.class鎖,初始化B時會獲得B.class鎖,而在初始化B時又需要A,但是這2個初始化都是在同一個線程里執(zhí)行的,該線程會同時獲得這2個鎖,因此并不會發(fā)生鎖資源的搶占,最終執(zhí)行結(jié)果為:

class A init.
class B init.
method test called in class A
method test called in class B
2.2 子類、父類初始化死鎖

與第一種情況相比,這種情況造成的死鎖會更隱蔽一點,但它們實質(zhì)上都是同樣的原因,來看個具體的例子:

public class Test { 

    public static class Parent {
        static {
            System.out.println("Parent init.");
        }

        public static final Parent EMPTY = new Child();
        
        public static void test() {
            System.out.println("test called in class Parent.");
        }
        
    }
    
    public static class Child extends Parent {      
        static {
            System.out.println("Child init.");
        }
    }
    
    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                Child c = new Child();
            }       
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                Parent.test();
            }       
        });
        t1.start();
        t2.start();
    }   
}

執(zhí)行結(jié)果為:

Parent init.

我們來分析下造成死鎖的原因:
1.線程t1執(zhí)行時會觸發(fā)Child類的初始化,線程t2執(zhí)行時會觸發(fā)Parent類的初始化;
2.緊接著線程t1持有Child.class鎖,t2持有Parent.class鎖,t1初始化時需要先初始化其父類Parent,而類Parent有個常量定義“public static final Parent EMPTY = new Child();”,這樣類Parent在初始化時需要初始化Child;
3.這樣線程t1要初始化Parent,嘗試獲取Parent.class鎖,線程t2要初始化Child,嘗試獲取Child.class鎖,彼此互相不能釋放資源,因此造成死鎖。

3.一個死鎖引發(fā)的血案

在曾經(jīng)開發(fā)的某一個Android項目中,采用了一個開源的ORM數(shù)據(jù)庫框架litepal來進(jìn)行數(shù)據(jù)庫操作,結(jié)果應(yīng)用上線之后,經(jīng)常有用戶反饋說時不時會出現(xiàn)卡死現(xiàn)象。后來經(jīng)過自己測試,也會偶發(fā)卡死現(xiàn)象,但是沒有一點規(guī)律可循,一直都無法定位到bug所在,導(dǎo)致被用戶投訴罵的很慘,這可急壞了開發(fā)人員。后來通過導(dǎo)出手機(jī)的anr文件,仔細(xì)分析之后,終于發(fā)現(xiàn)出現(xiàn)anr是因為litepal數(shù)據(jù)庫發(fā)生死鎖了。(注:litepal本身是一個很好用的Android ORM數(shù)據(jù)庫框架,大部分情況下都是很好用的,這里只是描述一下我們的使用場景。)

<pre>"main" tid=1 :
  | group="main" sCount=1 dsCount=0 obj=0x757e6598 self=0xab361100
  | sysTid=17006 nice=0 cgrp=default sched=0/0 handle=0xf7210b50
  | state=S schedstat=( 731900052 38102591 941 ) utm=53 stm=20 core=6 HZ=100
  | stack=0xff0dc000-0xff0de000 stackSize=8MB
  | held mutexes=
  at org.litepal.crud.DataSupport.findFirst(DataSupport.java:-1)
  - waiting to lock <0x005e5028> (a java.lang.Class<org.litepal.crud.DataSupport>) held by thread 27
  at ......


"RxCachedThreadScheduler-2" tid=27 :
  | group="main" sCount=1 dsCount=0 obj=0x12e751c0 self=0xab9ae8a8
  | sysTid=17097 nice=0 cgrp=default sched=0/0 handle=0xdbb46930
  | state=S schedstat=( 548637659 14253750 564 ) utm=50 stm=4 core=3 HZ=100
  | stack=0xdba44000-0xdba46000 stackSize=1038KB
  | held mutexes=
  kernel: (couldn't read /proc/self/task/17097/stack)
  native: #00 pc 00016998  /system/lib/libc.so (syscall+28)
  native: #01 pc 000f5e73  /system/lib/libart.so (_ZN3art17ConditionVariable4WaitEPNS_6ThreadE+82)
  native: #02 pc 002ae8b3  /system/lib/libart.so (_ZN3art7Monitor4LockEPNS_6ThreadE+394)
  native: #03 pc 002b140f  /system/lib/libart.so (_ZN3art7Monitor12MonitorEnterEPNS_6ThreadEPNS_6mirror6ObjectE+266)
  native: #04 pc 002e5747  /system/lib/libart.so (_ZN3art10ObjectLockINS_6mirror6ObjectEEC2EPNS_6ThreadENS_6HandleIS2_EE+22)
  native: #05 pc 00139bab  /system/lib/libart.so (_ZN3art11ClassLinker15InitializeClassEPNS_6ThreadENS_6HandleINS_6mirror5ClassEEEbb.part.593+90)
  native: #06 pc 0013aa97  /system/lib/libart.so (_ZN3art11ClassLinker17EnsureInitializedEPNS_6ThreadENS_6HandleINS_6mirror5ClassEEEbb+82)
  native: #07 pc 002bd76d  /system/lib/libart.so (_ZN3artL18Class_classForNameEP7_JNIEnvP7_jclassP8_jstringhP8_jobject+292)
  native: #08 pc 0024eca9  /system/framework/arm/boot.oat (Java_java_lang_Class_classForName__Ljava_lang_String_2ZLjava_lang_ClassLoader_2+132)
  at java.lang.Class.classForName!(Native method)
  - waiting to lock <0x0229fe4b> (a java.lang.Class<......database.AnnouncementInfo>) held by thread 36
  at java.lang.Class.forName(Class.java:324)
  at java.lang.Class.forName(Class.java:285)


"RxCachedThreadScheduler-4" tid=36 :
  | group="main" sCount=1 dsCount=0 obj=0x12c3ce80 self=0xab8ab088
  | sysTid=17229 nice=0 cgrp=default sched=0/0 handle=0xdab2b930
  | state=S schedstat=( 56642965 8922138 61 ) utm=4 stm=1 core=6 HZ=100
  | stack=0xdaa29000-0xdaa2b000 stackSize=1038KB
  | held mutexes=
  kernel: (couldn't read /proc/self/task/17229/stack)
  native: #00 pc 00016998  /system/lib/libc.so (syscall+28)
  native: #01 pc 000f5e73  /system/lib/libart.so (_ZN3art17ConditionVariable4WaitEPNS_6ThreadE+82)
  native: #02 pc 002ae8b3  /system/lib/libart.so (_ZN3art7Monitor4LockEPNS_6ThreadE+394)
  native: #03 pc 002b140f  /system/lib/libart.so (_ZN3art7Monitor12MonitorEnterEPNS_6ThreadEPNS_6mirror6ObjectE+266)
  native: #04 pc 002e5747  /system/lib/libart.so (_ZN3art10ObjectLockINS_6mirror6ObjectEEC2EPNS_6ThreadENS_6HandleIS2_EE+22)
  native: #05 pc 00139165  /system/lib/libart.so (_ZN3art11ClassLinker11VerifyClassEPNS_6ThreadENS_6HandleINS_6mirror5ClassEEE+336)
  native: #06 pc 00139c0d  /system/lib/libart.so (_ZN3art11ClassLinker15InitializeClassEPNS_6ThreadENS_6HandleINS_6mirror5ClassEEEbb.part.593+188)
  native: #07 pc 0013aa97  /system/lib/libart.so (_ZN3art11ClassLinker17EnsureInitializedEPNS_6ThreadENS_6HandleINS_6mirror5ClassEEEbb+82)
  native: #08 pc 002cdb8b  /system/lib/libart.so (_ZN3artL23Constructor_newInstanceEP7_JNIEnvP8_jobjectP13_jobjectArray+134)
  native: #09 pc 0024f0cd  /system/framework/arm/boot.oat (Java_java_lang_reflect_Constructor_newInstance___3Ljava_lang_Object_2+96)
  at java.lang.reflect.Constructor.newInstance!(Native method)
  - waiting to lock <0x005e5028> (a java.lang.Class<org.litepal.crud.DataSupport>) held by thread 27
  at com.google.gson.internal.ConstructorConstructor$3.construct(ConstructorConstructor.java:-1)
  at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:-1)
  at com.google.gson.internal.bind.TypeAdapterRuntimeTypeWrapper.read(TypeAdapterRuntimeTypeWrapper.java:-1)
  at com.google.gson.internal.bind.CollectionTypeAdapterFactory$Adapter.read(CollectionTypeAdapterFactory.java:-1)
  at com.google.gson.internal.bind.CollectionTypeAdapterFactory$Adapter.read(CollectionTypeAdapterFactory.java:-1)
  at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.read(ReflectiveTypeAdapterFactory.java:-1)
  at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:-1)
  at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.read(ReflectiveTypeAdapterFactory.java:-1)
  at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:-1)
  at com.google.gson.Gson.fromJson(Gson.java:-1)
  at com.google.gson.Gson.fromJson(Gson.java:-1)
  at com.google.gson.Gson.fromJson(Gson.java:-1)

在這里,我截取了anr文件里的相關(guān)內(nèi)容。從上面可以看到,線程t1在執(zhí)行DataSupport.findFirst()方法時,需要DataSupport.class鎖,而DataSupport.class鎖是被線程t27所占有,因此t1被一直阻塞著,由于t1是主線程,主線程被阻塞所以會出現(xiàn)anr現(xiàn)象。我們再看線程t27,發(fā)現(xiàn)它需要AnnouncementInfo.class鎖,而該鎖又被線程t36所占有。接著看線程t36,發(fā)現(xiàn)它又需要DataSupport鎖。看到這里,基本上就明白發(fā)生死鎖了。

DataSupport是litepal框架里定義的一個數(shù)據(jù)庫操作基礎(chǔ)類,AnnouncementInfo是我們自己定義的一個數(shù)據(jù)表類,它需要繼承自DataSupport類,我們來看一下相關(guān)定義:

//自動創(chuàng)建 AnnouncementInfo 數(shù)據(jù)表
public class AnnouncementInfo extends DataSupport {
    //數(shù)據(jù)表字段定義
}

DataSupport里findFirst()方法的定義:

public static synchronized <T> T findFirst(Class<T> modelClass);    

我們的應(yīng)用里創(chuàng)建了若干個不同的數(shù)據(jù)表,在操作數(shù)據(jù)庫的時候,都是采用異步調(diào)用的方式。以查詢AnnouncementInfo數(shù)據(jù)表為例,通常都這樣寫:

AnnouncementInfo data = DataSupport.findFirst(AnnouncementInfo.class);

直接這樣使用是沒有問題的,但是當(dāng)我們異步操作數(shù)據(jù)庫表,并且在其他子線程中操作AnnouncementInfo類時,就發(fā)生了問題,我們分析上面這個例子:
1.主線程執(zhí)行DataSupport.findFirst方法時,發(fā)現(xiàn)DataSupport類沒有初始化,則先嘗試獲取DataSupport.class鎖,只有獲得該鎖之后才能對其進(jìn)行初始化;
2.某個子線程在操作數(shù)據(jù)庫的時候,觸發(fā)了DataSupport類的初始化,初始化過程中發(fā)現(xiàn)有依賴AnnouncementInfo類,而AnnouncementInfo類此時并沒有初始化,于是嘗試獲得AnnouncementInfo.class鎖來初始化該類;
3.與此同時某個子線程采用Gson庫解析json數(shù)據(jù)生成AnnouncementInfo對象實例時,觸發(fā)了AnnouncementInfo類的初始化,但是初始化AnnouncementInfo類需要先初始化其父類DataSupport,而在第2個步驟里DataSupport類初始化時已被阻塞住了;
這樣就造成了循環(huán)依賴,并導(dǎo)致主線程阻塞,引起anr。

4.死鎖解決方法

在上面這個案例中,我們知道是類初始化時造成了死鎖。子類依賴了父類,而父類在初始化過程中又依賴了子類,為了避免這種情況,我們采取了預(yù)先在主線程中將數(shù)據(jù)庫相關(guān)類全部初始化的方式。
在應(yīng)用入口處,我們作了如下處理:

Class c1 = Class.forName("AnnouncementInfo");
Class c2 = Class.forName("......");
......

這樣在應(yīng)用啟動時,所有數(shù)據(jù)庫相關(guān)類都已經(jīng)初始化完成,當(dāng)我們異步操作數(shù)據(jù)庫時,再也不會出現(xiàn)上面提到的死鎖情況了。

5.小結(jié)

一般情況下,代碼出現(xiàn)死鎖是很難排查的,特別是在多線程環(huán)境下,尤其需要注意。但是只要們理解死鎖出現(xiàn)的根本原因,在實際開發(fā)中基本能避免了。

java類加載機(jī)制系列文章:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,791評論 6 545
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,795評論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,943評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,057評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 72,773評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,106評論 1 330
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,082評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,282評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,793評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,507評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,741評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,220評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,929評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,325評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,661評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,482評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 48,702評論 2 380

推薦閱讀更多精彩內(nèi)容

  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法,內(nèi)部類的語法,繼承相關(guān)的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,738評論 18 399
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,836評論 18 139
  • 在漫長的青春歲月中,我一直喜歡著的少年。在我的記憶中,他是干凈的,有微風(fēng)吹過來在他身上會散發(fā)出一種...
    A幺妹兒閱讀 244評論 0 0
  • 痛失了愛人的吻 痛失一條街,一座城 這吻像野貓一樣 掉進(jìn)灰色的瞳仁里 小心,踩著薄霧行走 痛失了自己的靈魂 沒有什...
    我是不是蝎大人閱讀 154評論 0 1
  • 早上我看到公婆在外面很忙,婆婆看著很累的樣子,我就對老公說,快點起來吧,你爸媽是在外面很忙。老公回了一句,我忙的時...
    自由綻放的我閱讀 123評論 0 0