本节例子内容较多~
四种方法概述
- 在静态初始化函数中初始化一个函数的引用
- 将对象的引用保存到volatile类型域AtomicReference对象中
- 将对象的引用保存到某个正确构造对象的final类型域中(后续再进行补充!)
- 将对象的引用保存到一个由锁保存的域中
代码演示
通过在Spring框架中构造线程安全且只被初始化一次的不同单例(Singleton)进行演示。
懒汉模式:(单例实例在第一次使用时进行创建)
1 | @NotThreadSafe |
分析一下:
- 单线程时使用没问题。
- 多线程时,当多个线程同时判断到instance == null,那么该多个线程都会创建一个实例,即非线程安全,因此该方法无法保证实例只被初始化一次。
饿汉模式:(单例实例在类装载时进行创建)
1 | @ThreadSafe |
分析一下:
- 该方式为线程安全。
- 不足:如果单例类的构造方法中有较多的处理逻辑,导致类加载慢,可能会引起性能问题。
- 由于是饿汉模式,如果只声明了该类但实际不调用该类,即造成系统资源的浪费。
改造的懒汉模式-1:synchronized标识工厂方法
1 | @ThreadSafe |
分析一下:
- 是线程安全的。
- 通过给工厂方法添加synchronized关键字实现。
- 不推荐使用:通过阻塞线程->牺牲性能,达到线程安全目的。
改造的懒汉模式-2:(双重同步锁单例模式)
1 | @NotThreadSafe |
分析一下:
- 非线程安全。
- 将synchronized标识下沉到方法的实现中
- 外层instance == null和方法实现中的synchronized标识共同保证只有一个线程进行初始化。
- 内层的instance == null为防止上一时刻中可能存在的线程进行了初始化。
非线程安全分析:
当执行实例初始化instance = new SingletonExample4()时,CPU内部过程为:
- memory = allocate() -> 分配对象的内存空间;
- ctorInstance() -> 初始化对象;
- instance = memory -> 设置instance指向刚分配的内存。
但是!!
在多线程环境
中,由于JVM和CPU优化,会发生指令重排,CPU内部顺序为:(其中的1、2、3是指令间的原始顺序)
- memory = allocate() -> 分配对象的内存空间;
- instance = memory -> 设置instance指向刚分配的内存。
- 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 | public class SingletonExample7 { |
分析一下:
- 枚举模式是最安全的!较推荐的写法!
- 通过枚举类中指定一个单例
Singleton
的实例INSTANCE
枚举实现。 - 其中
Singleton(){xxxx}
域,是通过JVM保证这个方法在被调用前初始化,并绝对只调用一次。 - 相比于懒汉模式,它的安全性更易保证;相比于饿汉模式,它是在实际调用时才进行初始化,并直接取到其值,不会有系统资源的占用浪费。