美文网首页
类初始化造成的死锁

类初始化造成的死锁

作者: 云飞扬1 | 来源:发表于2018-03-05 23:19 被阅读673次

    1.死锁是怎么产生的

    类初始化是一个很隐蔽的操作,是由虚拟机主导完成的,开发人员不了解类加载机制的话,可能压根不知道类初始化是个什么东东。类初始化的文章有专门讲过,可参考Java虚拟机类加载机制,里面有详细描述。
    关于类初始化有几个关键特性:

    • 类初始化的过程其实就是执行类构造器方法<clinit>()的过程;
    • 在子类初始化完成时,虚拟机会保证其父类有初始化完成;
    • 多线程环境下,虚拟机执行<clinit>()方法会自动加锁;

    在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数据库框架,大部分情况下都是很好用的,这里只是描述一下我们的使用场景。)

    <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文件里的相关内容。从上面可以看到,线程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> T findFirst(Class<T> 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类加载机制系列文章:

    相关文章

      网友评论

          本文标题:类初始化造成的死锁

          本文链接:https://www.haomeiwen.com/subject/dqatbxtx.html