类初始化造成的死锁

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类加载器

你可能感兴趣的:(类初始化造成的死锁)