安对象安全发布的四种策略

本节例子内容较多~

四种方法概述

  • 在静态初始化函数中初始化一个函数的引用
  • 将对象的引用保存到volatile类型域AtomicReference对象中
  • 将对象的引用保存到某个正确构造对象的final类型域中(后续再进行补充!)
  • 将对象的引用保存到一个由锁保存的域中

    代码演示

通过在Spring框架中构造线程安全且只被初始化一次的不同单例(Singleton)进行演示。

懒汉模式:(单例实例在第一次使用时进行创建
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@NotThreadSafe
public class SingletonExample1 {

// 私有构造函数
private SingletonExample1() {
}

// 单例对象
private static SingletonExample1 instance = null;

// 静态的工厂方法
public static SingletonExample1 getInstance() {
if (instance == null) {
instance = new SingletonExample1();
}
return instance;
}
}

分析一下:

  1. 单线程时使用没问题。
  2. 多线程时,当多个线程同时判断到instance == null,那么该多个线程都会创建一个实例,即非线程安全,因此该方法无法保证实例只被初始化一次。

饿汉模式:(单例实例在类装载时进行创建)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@ThreadSafe
public class SingletonExample2 {

// 私有构造函数
private SingletonExample2() {

}

// 单例对象
private static SingletonExample2 instance = new SingletonExample2();

// 静态的工厂方法
public static SingletonExample2 getInstance() {
return instance;
}
}

分析一下:

  1. 该方式为线程安全。
  2. 不足:如果单例类的构造方法中有较多的处理逻辑,导致类加载慢,可能会引起性能问题。
  3. 由于是饿汉模式,如果只声明了该类但实际不调用该类,即造成系统资源的浪费。

改造的懒汉模式-1:synchronized标识工厂方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@ThreadSafe
@NotRecommend
public class SingletonExample3 {

// 私有构造函数
private SingletonExample3() {
}

// 单例对象
private static SingletonExample3 instance = null;

// 静态的工厂方法
public static synchronized SingletonExample3 getInstance() {
if (instance == null) {
instance = new SingletonExample3();
}
return instance;
}
}

分析一下:

  1. 是线程安全的。
  2. 通过给工厂方法添加synchronized关键字实现。
  3. 不推荐使用:通过阻塞线程->牺牲性能,达到线程安全目的。

改造的懒汉模式-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
@NotThreadSafe
public class SingletonExample4 {

// 私有构造函数
private SingletonExample4() {
}

// JVM和cpu优化,发生了指令重排

// 1、memory = allocate() 分配对象的内存空间
// 3、instance = memory 设置instance指向刚分配的内存
// 2、ctorInstance() 初始化对象

// 单例对象
private static SingletonExample4 instance = null;

// 静态的工厂方法
public static SingletonExample4 getInstance() {
if (instance == null) { // 双重检测机制 // B
synchronized (SingletonExample4.class) { // 同步锁
if (instance == null) {
instance = new SingletonExample4(); // A - 3
}
}
}
return instance;
}
}

分析一下:

  1. 非线程安全
  2. 将synchronized标识下沉到方法的实现中
  3. 外层instance == null和方法实现中的synchronized标识共同保证只有一个线程进行初始化。
  4. 内层的instance == null为防止上一时刻中可能存在的线程进行了初始化。
非线程安全分析:

当执行实例初始化instance = new SingletonExample4()时,CPU内部过程为:

  1. memory = allocate() -> 分配对象的内存空间;
  2. ctorInstance() -> 初始化对象;
  3. instance = memory -> 设置instance指向刚分配的内存。

但是!!
多线程环境中,由于JVM和CPU优化,会发生指令重排,CPU内部顺序为:(其中的1、2、3是指令间的原始顺序)

  1. memory = allocate() -> 分配对象的内存空间;
  2. instance = memory -> 设置instance指向刚分配的内存。
  3. ctorInstance() -> 初始化对象;

因此,此时的双重同步锁机制中产生了变化:

若两个线程A和B,其中线程A执行到初始化instance = new SingletonExample4(),此时恰好正处于指令重排的instance = memory ,即设置instance指向刚分配的内存,同时线程B恰好处于外层的instance == null判断,发现此时内存中该instance指向的内存地址不为nul,则会直接return instance,但此时的instance只是分配了内存还未进行初始化,即产生错误!为非线程安全!!

但是!!!!!
既然是内存中实例未完全初始化,怎么解决呢??
想起来之前说过的volatile关键字的用法了没???
它通过加入内存屏障,限制JVM或CPU进行指令重排!!

将该实例用volatile标识声明:

1
private volatile static SingletonExample5 instance = null;

此时,该双重同步锁单例模式就是线程安全的了!!


实例枚举模式:
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
public class SingletonExample7 {

// 私有构造函数
private SingletonExample7() {
}

public static SingletonExample7 getInstance() {
return Singleton.INSTANCE.getInstance();
}

//枚举类
private enum Singleton {
INSTANCE;

private SingletonExample7 singleton;

Singleton() {
singleton = new SingletonExample7();
}

public SingletonExample7 getInstance() {
return singleton;
}
}
}

分析一下:

  1. 枚举模式是最安全的!较推荐的写法!
  2. 通过枚举类中指定一个单例Singleton的实例INSTANCE枚举实现。
  3. 其中Singleton(){xxxx}域,是通过JVM保证这个方法在被调用前初始化,并绝对只调用一次。
  4. 相比于懒汉模式,它的安全性更易保证;相比于饿汉模式,它是在实际调用时才进行初始化,并直接取到其值,不会有系统资源的占用浪费。
SupriseMF wechat
欢迎关注微信订阅号【星球码】,分享学习编程奇淫巧技~
喜欢就支持我呀(*^∇^*)~

热评文章