多线程环境下的单例模式
2018-05-21 本文已影响9人
8813d76fee36
记录一下单例模式的几个演化版本。
版本1 - 单线程下正确的版本
public class SingleThreadedSingleton {
/**
* 保存该类的唯一实例
*/
private static SingleThreadedSingleton instance;
/**
* 私有化构造方法
*/
private SingleThreadedSingleton() {}
/**
* 创建并返回实例
* @return
*/
public static SingleThreadedSingleton getInstance() {
if (instance == null) { // if判断在多线程环境下形成 read-check-act 操作,非原子操作
instance = new SingleThreadedSingleton();
}
return instance;
}
}
- 思路说明
该版本是最原始的单例模式实现代码。使用静态成员变量保存类的实例;私有化构造方法,使得外界无法创建它的实例;向外界提供公开的获取实例的方法。
在获取实例方法getInstance()
中,首先判断是否已经存在实例:若不存在则创建一个实例,存在则直接返回。采用了懒加载思想,即当用到的时候才创建实例。 - 问题分析
该版本在单线程环境下可以正常使用,但在多线程环境下就会有问题。
// if判断在多线程环境下形成 read-check-act 操作,非原子操作
if (instance == null) {
instance = new SingleThreadedSingleton();
}
if判断是一个read-check-act
(读取-判断-执行)操作,if之后的操作是否执行取决于前面的判断结果,而read-check-act
是非原子操作,因此可能发生如下情况:
线程T1执行if判断,此时instance为null,T1进入到if体但还没有执行创建实例的语句;此时线程T2也执行if判断,instance仍然为null,T2也进入到if体。之后T1,T2分别创建了两个实例。
- 解决
上述问题的关键就是if语句判断的时候产生了非原子操作,可以使用synchronized关键字使操作原子化。
版本2 - 同步方法
public class SynchronizedSingleton {
private static SynchronizedSingleton instance;
private SynchronizedSingleton() {}
synchronized public static SynchronizedSingleton getInstance() {
if (instance == null) {
instance = new SynchronizedSingleton();
}
return instance;
}
}
- 思路说明
在版本1问题(包含非原子操作,且没有引入同步)的基础上,将getInstance()
方法使用synchronized
关键字修饰,成为同步方法,此时该方法一次只能有一个线程进入,解决了版本1中的问题。 - 问题分析
版本2虽然使用同步方法保证该方法一次只有一个线程执行,保证了实例只被创建一次,但每次线程进来都需要申请锁,加大了系统开销。 - 解决
使用双重检查,并配合同步代码块,降低锁的申请,提升性能。
版本3 - 双重检查
public class DoubleCheckSingleton {
private static volatile DoubleCheckSingleton instance;
private DoubleCheckSingleton() {}
public static DoubleCheckSingleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (DoubleCheckSingleton.class) {
if (instance == null) { // 第二次检查
instance = new DoubleCheckSingleton();
}
}
}
return instance;
}
}
- 思路说明
为了防止每次进入getInstance()
方法都要申请锁,我们采用双重检查机制,即先检查instance
是否为null
,如果不为null
则直接返回该实例;如果为null
,则进入同步代码块,在同步代码块中,为了防止外层if
语句出现版本1中提到的check-then-act
问题,我们在同步代码块中再次检查instance
是否为null
,若不为null
则创建该实例。 - 注意
类中保存的唯一实例变量instance
需要使用关键字volatile
修饰,否则可能依然会存在问题。
private static volatile DoubleCheckSingleton instance;
- 问题说明
如果唯一实例变量不使用volatile
修饰会出现什么问题呢?
实例化操作instance = new DoubleCheckSingleton();
并不是一个原子操作,在编译后该操作可分解为三个子操作:
ref = allocate(DoubleCheckSingleton.class); // 1. 分配创建对象所用内存空间
invoke(ref); // 2. 初始化ref引用的对象
instance = ref; // 3. 将对象引用写入变量
上述操作在实际执行时可能发生指令重排(临界区内操作可在临界区内重排),导致执行顺序可能是 1 -> 3 -> 2 。
如以下情景:
线程T1通过双检查发现instance
为null
,即开始执行instance = new DoubleCheckSingleton();
操作,但此时发生了指令重排执行顺序是 1 -> 3 -> 2,当执行到3时,对于变量instance
来说已经不为null
了,但此时该变量引用的实例并没有初始化完毕,即该实例的实例变量都是默认值而不是构造器设置的初始值,同时线程T2执行到外层第一次if
检查,发现instance
变量已经不为null
,直接返回,则这次返回的就是T1创建的不完全实例化的对象实例。
- 问题解决
解决以上问题的办法就是为实例变量instance
添加volatile
修饰。
private static volatile DoubleCheckSingleton instance;
volatile
起到了两个作用:
- 保障可见性
一个线程修改了instance
变量的值,其他线程可以看到该线程对它所做的修改。 - 保障有序性
volatile
变量能够禁止被修饰的变量的写操作于之前的任何读写操作发生指令重排,因此可以避免实例化子操作3被重排到操作2之前执行,保障了所有线程获取到的实例都是完全初始化的。
版本4 - 使用静态内部类保存外部类实例
public class InnerClassSingleton {
// 私有化构造方法
private InnerClassSingleton() {}
// 使用静态内部类保存外部类实例
static class InstanceHolder {
static final InnerClassSingleton INSTANCE = new InnerClassSingleton();
}
public static InnerClassSingleton getInstance() {
return InstanceHolder.INSTANCE;
}
}
- 思路说明
静态变量会在被初次访问的时候被实例化,且只会被实例化一次。当调用getInstance()
方法时,首先访问了静态内部类InstanceHolder
,使其被初始化。由于内部类中使用静态成员常量保存外部类的实例,因此外部类实例的初始化操作也随着内部类的初始化操作而执行,且只会被执行一次。
版本5 - 使用枚举(推荐)
public enum EnumSingleton {
INSTANCE;
EnumSingleton() {
}
}
利用枚举自身是单例的特点,使用枚举类来创建目标类的单例。