类初始化造成的死锁

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类加载机制系列文章:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,493评论 18 399
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,490评论 18 139
  • 在漫长的青春岁月中,我一直喜欢着的少年。在我的记忆中,他是干净的,有微风吹过来在他身上会散发出一种...
    A幺妹儿阅读 227评论 0 0
  • 痛失了爱人的吻 痛失一条街,一座城 这吻像野猫一样 掉进灰色的瞳仁里 小心,踩着薄雾行走 痛失了自己的灵魂 没有什...
    我是不是蝎大人阅读 144评论 0 1
  • 早上我看到公婆在外面很忙,婆婆看着很累的样子,我就对老公说,快点起来吧,你爸妈是在外面很忙。老公回了一句,我忙的时...
    自由绽放的我阅读 117评论 0 0