Java-基础-类初始化造成的死锁

Java-基础-类初始化造成的死锁

前言

关于类初始化有几个关键特性:

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

在java中,死锁肯定是在多线程环境下产生的。多个线程同时需要互相持有的某个资源,自己的资源无法释放,别人的资源又无法得到,造成循环依赖,进而一直阻塞在那里,这样就形成死锁了。

1. 两个类初始化互相依赖

  • 最明显的情况是,2个类在不同的线程中初始化,彼此互相依赖,我们来看个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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();
}
}

运行结果如下:

1
2
class A init.
class B init.

解释:

1
2
3
4
1. 第一个线程执行A.test()的时候,开始初始化类A,该线程获得A.class的锁,第二个线程执行B.test()的时候,开始初始化类B,该线程获得B.class的锁。
2. A在初始化过程中执行代码B b = new B()的时候,发现类B还没有初始化完成,于是尝试获得类B.class的锁;
3. 类B在初始化时执行代码A a = new A(),发现类A也没有初始化完成,于是尝试获得类A.class的锁,但A.class锁已被占用,所以该线程会阻塞住,并等待该锁的释放;
4. 同样第一个线程阻塞住并等待B.class锁的释放,这样就造成循环依赖,形成了死锁。
  • 如果把上面代码改为如下执行方式,会出现什么结果呢?
1
2
3
4
public static void main(String[] args) {
A.test();
B.test();
}

最终执行结果为:

1
2
3
4
class A init.
class B init.
method test called in class A
method test called in class B

解释

1
2
3
1. 乍一看去,好像A初始化时依赖B,B初始化时依赖A,也会造成死锁,但实际上并不会。
2. A、B两个类的初始化都是在同一个线程里执行的,初始化A的时候,该线程会获得A.class锁,初始化B时会获得B.class锁,而在初始化B时又需要A
3. 但是这2个初始化都是在同一个线程里执行的,该线程会同时获得这2个锁,因此并不会发生锁资源的抢占

2. 子类,父类初始化造成死锁

  • 与第一种情况相比,这种情况造成的死锁会更隐蔽一点,但它们实质上都是同样的原因,来看个具体的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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();
}
}

执行结果为:

1
Parent init.

我们来分析下造成死锁的原因:

  • 线程t1执行时会触发Child类的初始化,线程t2执行时会触发Parent类的初始化;
  • 紧接着线程t1持有Child.class锁,t2持有Parent.class锁,t1初始化时需要先初始化其父类Parent
  • 而类Parent有个常量定义“public static final Parent EMPTY = new Child();”,这样类Parent在初始化时需要初始化Child;
  • 这样线程t1要初始化Parent,尝试获取Parent.class锁,线程t2要初始化Child,尝试获取Child.class锁,彼此互相不能释放资源,因此造成死锁。

3. 解决方案

  • 在上面这个案例中,我们知道是类初始化时造成了死锁。子类依赖了父类,而父类在初始化过程中又依赖了子类,为了避免这种情况,
  • 我们采取了预先在主线程中将数据库相关类全部初始化的方式。
    在应用入口处,我们作了如下处理:
1
2
3
Class c1 = Class.forName("AnnouncementInfo");
Class c2 = Class.forName("......");
......
  • 这样在应用启动时,所有相关类都已经初始化完成(一次性分配了所有资源)
打赏
  • 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!
  • © 2019-2022 Zhuuu
  • PV: UV:

请我喝杯咖啡吧~

支付宝
微信