Java并发编程艺术 - 延迟加载与双重检查锁定double check lock

类延迟初始化常采用单例模式。

方法1:

1
2
3
4
5
6
7
8
9
10
public class Singleton {
private static Singleton singleton = null;

public static Singleton getInstance() {
if (singleton == null) {//1
singleton=new Singleton();//2
}
return singleton;
}
}

多线程中,步骤1可以同步执行,导致步骤2多次执行。

方法2:

1
2
3
4
5
6
7
8
9
10
public class Singleton {
private static Singleton singleton = null;

public static synchronized Singleton getInstance() {
if (singleton == null) {//1
singleton=new Singleton();//2
}
return singleton;
}
}

多线程中,singleton只会实例一次,但每次获取getInstance都会加锁导致性能下降。

方法3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Singleton {
private static Singleton singleton = null;

public static Singleton getInstance() {
//one check
if (singleton == null) {//1
synchronized (Singleton.class) {//2
//two check
if (singleton == null) {//3
singleton = new Singleton();//4
}
}
}
return singleton;
}
}

步骤4分解为3条指令执行,包括1-先分配内存,2-标记对象指向内存地址,3-初始化构造;如果指令12重排导致先赋值,多线程并发中步骤1不为null,但获取的singleton仍未初始化。

方法4:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Singleton {
//防止重排
private volatile static Singleton singleton = null;//5

public static Singleton getInstance() {
//one check
if (singleton == null) {//1
synchronized (Singleton.class) {//2
//two check
if (singleton == null) {//3
singleton = new Singleton();//4
}
}
}
return singleton;
}
}

步骤5使用volatile防止指令重排,是标准的DCL实例方案。

方法5:

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
43
44
45
46
47
48
public class Singleton implements Serializable{//序列化
//防止重排
private volatile static Singleton singleton = null;//5
private static boolean flag = false;

private Singleton() {//6
//防止反射漏洞
synchronized (Singleton.class) {
if (flag) {
System.out.println("init twice");
} else {
flag = true;
System.out.println("init once");
}
}
}

public static Singleton getInstance() {
//one check
if (singleton == null) {//1
synchronized (Singleton.class) {//2
//two check
if (singleton == null) {//3
singleton = new Singleton();//4
}
}
}
return singleton;
}

//防止反序列化漏洞,替换反序列化对象
private Object readResolve() {
return singleton;
}

public static void main(String[] args) throws Exception {
Class<Singleton> clazz = (Class<Singleton>) Class.forName("Singleton");
Constructor<Singleton> singletonConstructor = clazz.getDeclaredConstructor(null);
singletonConstructor.setAccessible(true);//反射打开访问权限

Singleton singleton = singletonConstructor.newInstance();//第一次实例化
singleton.say();

System.out.println(singleton);
singleton = singletonConstructor.newInstance();//第二次实例化
singleton.say();
}
}

步骤6对构造函数进行私有化,防止多次被实例化;同时,为防止反射多次实例,使用全局变量flag;也支持单例模式的序列化和反序列化。在执行类的初始化期间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。

方法6:

1
2
3
4
5
6
7
8
9
10
11
public class Singleton {
private static Singleton singleton = null;

private static class SingletonFactory {
public static Singleton singleton2 = new Singleton();
}

public static Singleton getInstance() {
return SingletonFactory.singleton2;
}
}

利用类加载机制来实现,延迟初始化,类加载分为加载->验证->准备->解析->初始化->使用->卸载等步骤,Java中当类的静态域或静态方法被引用的时候,必须对声明这个静态域或方法的类进行初始化。

方法7:

1
2
3
4
5
6
7
8
9
10
11
12
public enum SingletonEnum {
INSTANCE_ENUM;
DataSource dataSource;

private SingletonEnum() {
dataSource = null;
}

public DataSource getDataSource() {
return dataSource;
}
}

利用枚举类实现单例模式。SingletonEnum会被编译成public final class SingletonEnum extends Enum,INSTANCE_ENUM会编译成public static final SingletonEnum INSTANCE_ENUM,根据类加载机制,初始化是线程安全的;同时,枚举序列化和反序列化禁止writeObject、readObject,不采用反射而是根据name属性和valueOf方法,防止破坏单例。

------ 本文结束------

本文标题:Java并发编程艺术 - 延迟加载与双重检查锁定double check lock

文章作者:Perkins

发布时间:2019年05月30日

原始链接:https://perkins4j2.github.io/posts/31866/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。