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ī)制系列文章: