单例模式的正确实现
前言
单例模式的实现目标:保持一个类有且仅有一个实例
基本要点:
- 必须有一个
private
的构造函数 instance
和getInstance()
必须是static
PS: 序列化和反序列化可能会破坏单例, 但这种场景并不常见,如果存在需要多加注意, (防止序列化破坏单例)
另, 理论上反射这也会破坏单例,一般不会这样做。
目录
如果想直接看正确的单例,移步目录中的 2.2 , 3 , 4
单线程版单例模式
public class SingleTest {
//保存该类的唯一实例
private static SingleTest instance;
//省略实例变量...
//保证该类不能被new
private SingleTest() {}
//创建并返回实例,在需要时(在调用getInstance时)才会被创建
public static SingleTest getInstance(){
if (null == instance) { //操作 1
instance = new SingleTest(); 操作 2
}
return instance;
}
public void someService(){
//...
}
}
在多线程环境下,getInstance
中的if
语句不是一个原子操作,代码中未使用任何同步机制,可能出现线程交错的情况。
当instance为null的时候两个(或两个以上)线程同时进入了if条件,则会创建两个(或两个以上)的实例,这显然违背了单例模式的初衷, 当然不难想到,可通过加锁解决这个问题
简单加锁实现的单例模式
public class SingleTest {
//保存该类的唯一实例
private static SingleTest instance;
//省略实例变量...
//保证该类不能被new
private SingleTest() {}
//创建并返回实例,在需要时(在调用getInstance时)才会被创建
public static SingleTest getInstance(){
synchronized (SingleTest.class) {
if (null == instance) {
instance = new SingleTest();
}
}
return instance;
}
public void someService(){
//...
}
}
加锁固然是线程安全的,但getInstance
的任何一个执行线程都需要申请锁,为了避免锁的开销,可以使用双重检查锁定(Double-checked Locking,DCL)
DCL (双重检查锁定)目前已被视为反模式(Anti-Pattern) 即不再提倡使用,但不少系统和框架还在使用这种方法,因此掌握这种方法可能失效的原因和正确的实现仍然具有实际意义
基于双重检查锁定的错误单例模式实现
public class SingleTest {
//保存该类的唯一实例
private static SingleTest instance;
//省略实例变量...
//保证该类不能被new
private SingleTest() {}
//创建并返回实例,在需要时(在调用getInstance时)才会被创建
public static SingleTest getInstance(){
if (null == instance) { //操作 1 第一次检查
synchronized (SingleTest.class) {
if (null == instance) { //造作 2 第二次检查
instance = new SingleTest(); //操作 3
}
}
}
return instance;
}
public void someService(){
//...
}
}
以上示例看起来即避免了锁的开销又保障了线程安全,仅仅从线程的可见性角度分析的确如此,但在一些情形下确保线程安全光考虑可见性是不够的。还需要考虑到 java内存模型的重排序因素
我们知道操作3可以分解为以下代码所示的几个独立子操作
objRef = allocate(SingleTest.class); //子操作1:分配对象所需内存空间
invokeConstructor(objRef); // 子操作2:初始化对象
instance = objRef; // 子操作3:将对象的引用写入instance共享变量(instance指向刚刚分配的内存地址)
JIT编译器可能将上述操作重排序为: 子操作1->子操作3->子操作2, 即在初始化对象之前将对象的引用写入实例变量instance
,那么就可能会有线程看到的instance
是一个不为null但初始化未完成的对象并将其直接返回,由于这个对象未初始化完成直接返回可能导致程序出错
解决方法: 将instance变量采用volatile
修饰
实际上是利用了volatile
的以下2个作用:
- 保障可见性:
- 一个线程通过执行子操作3修改了instance的变量值,其他线程可以读取到响应的值(通过子操作1)
- 保障有序性:
- 由于
volatile
能够禁止volatile
变量写操作 与 该操作之前的任何读、写操作进行重排序,因此volatile
修饰的instance相当于禁止JIT编译器以及处理器将子操作2(对象进行初始化的写操作)重排序到子操作3(将对象引用写入共享变量的写操作)之后。 这保障了一个线程读取到的instance变量所引用的实例的时候该实例已经初始化完成
- 由于
基于双重检查锁定的正确单例模式的实现
public class SingleTest {
//保存该类的唯一实例
private static volatile SingleTest instance;
//省略实例变量...
//保证该类不能被new
private SingleTest() {}
//创建并返回实例,在需要时(在调用getInstance时)才会被创建
public static SingleTest getInstance(){
if (null == instance) { //操作 1 第一次检查
synchronized (SingleTest.class) {
if (null == instance) { //造作 2 第二次检查
instance = new SingleTest(); //操作 3
}
}
}
return instance;
}
public void someService(){
//...
}
}
基于静态内部类的单例模式实现
考虑到双重检测锁定法是想上容易出错,我们可以采用另外一种同样可以实现延迟加载且比较简单的一种方法
public class SingleTest {
//保证该类不能被new
private SingleTest() {}
//私有的静态内部类
privare static class InstanceHolder(){
//保存外部类的唯一实例
finale static SingleTest INSTANCE = new SingleTest();
}
//创建并返回实例,在需要时(在调用getInstance时)才会被创建
public static SingleTest getInstance(){
return InstanceHolder.INSTANCE;
}
public void someService(){
//...
}
}
SingleTest
被载入JVM时,其内部类并不会被初始化,getInstance()
被调用时,InstanceHolder
内部类才会被加载,进而加载实例INSTANCE
类的静态变量被初次访问会触发java虚拟机对该类进行初始化,即该类的静态变量的值会变成其初始值而非默认值。 由于类的静态变量只会创建一次,因此SingleTest
只会被创建一次(单例)
基于枚举类型的单例模式
正确实现延迟加载的单例模式还有一种更为简洁的方法, 就是利用枚举类型
public enum Singleton{
INSTANCE;
public void someService(){}
}
调用:
Singleton.INSTANCE.someService();
枚举的所有成员默认都是public static final
修饰, 默认构造方法也是private
. 所以用枚举来实现单例模式是最简洁的.
这里的枚举类型相当于一个单例类,其字段INSTANCE相当于该类的唯一实例,这个实例是在Singleton.INSTANCE
初次被调用的时候才被初始化的,仅仅访问Singleton本身(比如Singleton.class.getName()
)并不会导致Singleton的唯一实例被初始化
完。