1.死锁是怎么产生的
类初始化是一个很隐蔽的操作,是由虚拟机主导完成的,开发人员不了解类加载机制的话,可能压根不知道类初始化是个什么东东。类初始化的文章有专门讲过,可参考Java虚拟机类加载机制,里面有详细描述。
关于类初始化有几个关键特性:
- 类初始化的过程其实就是执行类构造器方法
()的过程; - 在子类初始化完成时,虚拟机会保证其父类有初始化完成;
- 多线程环境下,虚拟机执行
()方法会自动加锁;
在java中,死锁肯定是在多线程环境下产生的。多个线程同时需要互相持有的某个资源,自己的资源无法释放,别人的资源又无法得到,造成循环依赖,进而一直阻塞在那里,这样就形成死锁了。
2.产生死锁的情况
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();
}
}
运行结果如下:
class A init.
class B init.
第一个线程执行A.test()的时候,开始初始化类A,该线程获得A.class的锁,第二个线程执行B.test()的时候,开始初始化类B,该线程获得B.class的锁。当A在初始化过程中执行代码B b = new B()的时候,发现类B还没有初始化完成,于是尝试获得类B.class的锁;类B在初始化时执行代码A a = new A(),发现类A也没有初始化完成,于是尝试获得类A.class的锁,但A.class锁已被占用,所以该线程会阻塞住,并等待该锁的释放;同样第一个线程阻塞住并等待B.class锁的释放,这样就造成循环依赖,形成了死锁。
如果把上面代码改为如下执行方式,会出现什么结果呢?
public static void main(String[] args) {
A.test();
B.test();
}
乍一看去,好像A初始化时依赖B,B初始化时依赖A,也会造成死锁,但实际上并不会。A、B两个类的初始化都是在同一个线程里执行的,初始化A的时候,该线程会获得A.class锁,初始化B时会获得B.class锁,而在初始化B时又需要A,但是这2个初始化都是在同一个线程里执行的,该线程会同时获得这2个锁,因此并不会发生锁资源的抢占,最终执行结果为:
class A init.
class B init.
method test called in class A
method test called in class B
2.2 子类、父类初始化死锁
与第一种情况相比,这种情况造成的死锁会更隐蔽一点,但它们实质上都是同样的原因,来看个具体的例子:
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();
}
}
执行结果为:
Parent init.
我们来分析下造成死锁的原因:
1.线程t1执行时会触发Child类的初始化,线程t2执行时会触发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.一个死锁引发的血案
在曾经开发的某一个Android项目中,采用了一个开源的ORM数据库框架litepal来进行数据库操作,结果应用上线之后,经常有用户反馈说时不时会出现卡死现象。后来经过自己测试,也会偶发卡死现象,但是没有一点规律可循,一直都无法定位到bug所在,导致被用户投诉骂的很惨,这可急坏了开发人员。后来通过导出手机的anr文件,仔细分析之后,终于发现出现anr是因为litepal数据库发生死锁了。(注:litepal本身是一个很好用的Android ORM数据库框架,大部分情况下都是很好用的,这里只是描述一下我们的使用场景。)
"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) 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) 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文件里的相关内容。从上面可以看到,线程t1在执行DataSupport.findFirst()方法时,需要DataSupport.class锁,而DataSupport.class锁是被线程t27所占有,因此t1被一直阻塞着,由于t1是主线程,主线程被阻塞所以会出现anr现象。我们再看线程t27,发现它需要AnnouncementInfo.class锁,而该锁又被线程t36所占有。接着看线程t36,发现它又需要DataSupport锁。看到这里,基本上就明白发生死锁了。
DataSupport是litepal框架里定义的一个数据库操作基础类,AnnouncementInfo是我们自己定义的一个数据表类,它需要继承自DataSupport类,我们来看一下相关定义:
//自动创建 AnnouncementInfo 数据表
public class AnnouncementInfo extends DataSupport {
//数据表字段定义
}
DataSupport里findFirst()方法的定义:
public static synchronized T findFirst(Class modelClass);
我们的应用里创建了若干个不同的数据表,在操作数据库的时候,都是采用异步调用的方式。以查询AnnouncementInfo数据表为例,通常都这样写:
AnnouncementInfo data = DataSupport.findFirst(AnnouncementInfo.class);
直接这样使用是没有问题的,但是当我们异步操作数据库表,并且在其他子线程中操作AnnouncementInfo类时,就发生了问题,我们分析上面这个例子:
1.主线程执行DataSupport.findFirst方法时,发现DataSupport类没有初始化,则先尝试获取DataSupport.class锁,只有获得该锁之后才能对其进行初始化;
2.某个子线程在操作数据库的时候,触发了DataSupport类的初始化,初始化过程中发现有依赖AnnouncementInfo类,而AnnouncementInfo类此时并没有初始化,于是尝试获得AnnouncementInfo.class锁来初始化该类;
3.与此同时某个子线程采用Gson库解析json数据生成AnnouncementInfo对象实例时,触发了AnnouncementInfo类的初始化,但是初始化AnnouncementInfo类需要先初始化其父类DataSupport,而在第2个步骤里DataSupport类初始化时已被阻塞住了;
这样就造成了循环依赖,并导致主线程阻塞,引起anr。
4.死锁解决方法
在上面这个案例中,我们知道是类初始化时造成了死锁。子类依赖了父类,而父类在初始化过程中又依赖了子类,为了避免这种情况,我们采取了预先在主线程中将数据库相关类全部初始化的方式。
在应用入口处,我们作了如下处理:
Class c1 = Class.forName("AnnouncementInfo");
Class c2 = Class.forName("......");
......
这样在应用启动时,所有数据库相关类都已经初始化完成,当我们异步操作数据库时,再也不会出现上面提到的死锁情况了。
5.小结
一般情况下,代码出现死锁是很难排查的,特别是在多线程环境下,尤其需要注意。但是只要们理解死锁出现的根本原因,在实际开发中基本能避免了。
java类加载机制系列文章:
- Java虚拟机类加载机制
- Java Class文件结构解析
- 类初始化造成的死锁
- Java类加载器