简介:本文将介绍几种单例模式的几种写法,并分析其在Spring中的应用。
创建型
单例模式(Singleton pattern)
定义
单例模式,也叫单子模式,单例对象的类必须保证只有一个实例存在。
常用情况
- 抽象工厂模式、建造者模式、原型模式可以在其实现中使用单例。
- 外观模式(Facade pattern)对象通常是单例的,提供一个对外的接口。
实现
饿汉式 - 线程安全(thread-safe)
在初始化变量时创建实例,因此不存在线程不安全的问题。
public final class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
}
懒汉式(Lazy initialization) - 线程不安全(not-thread-safe)
延迟加载的好处在于如果没用到该类,则不会实例化。此实现在多线程的环境下是线程不安全的,当多线程同时进入instance判断时,可能都会执行new Singleton() 语句,这将会导致获取的实例不同而产生问题
public class Singleton {
private static Singleton INSTANCE = null;
private Singleton() { }
public Singleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
return INSTANCE;
}
}
懒汉式(Lazy initialization) - 线程安全(thread-safe)
synchronized 关键字保证了多线程情况下只能有一个线程进入该方法,其他线程试图进入时会被阻塞,虽然保证了实例的唯一性,但是性能会受到影响,因此不推荐使用。
public class Singleton {
private static Singleton INSTANCE = null;
private Singleton() { }
public synchronized Singleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
return INSTANCE;
}
}
双重校验锁(double-checked locking) - 线程安全(thread-safe)
双重检查锁定模式(也被称为"双重检查加锁优化","锁暗示"(Lock hint)) 是一种软件设计模式用来减少并发系统中竞争和同步的开销。双重检查锁定模式首先验证锁定条件(第一次检查),只有通过锁定条件验证才真正的进行加锁逻辑并再次验证条件(第二次检查),具体实现如下。
public class Singleton {
private static volatile Singleton INSTANCE = null;
private Singleton() {}
public static Singleton getInstance() {
if (INSTANCE == null) {
synchronized (Singleton.class) {
if (INSTANCE == null){
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
3的 方法同步
会造成额外的开销,因此有以下优化方式:
- 检查变量是否被初始化(不去获得锁),如果已被初始化立即返回这个变量。
- 获取锁
- 第二次检查变量是否已经被初始化:如果其他线程曾获取过锁,那么变量已被初始化,返回初始化的变量。
- 否则,初始化并返回变量。
Java中,这看上去是一种比较有效的解决方案,然而实际上还需要考虑一些细微的问题。INSTANCE = new Singleton()并不是一个原子操作,其实际上可以抽象为下面几条JVM指令:
- memory =allocate(); //1:分配对象的内存空间
- ctorInstance(memory); //2:初始化对象
- instance =memory; //3:将 INSTANCE 指向刚分配的内存地址
我们希望指令按照 1->2->3
的顺序执行,然而JVM为了提高程序运行效率,会在不影响单线程程序执行结果的前提下,尽可能地提高并行度,也就是所谓的指令重排序,这里暂不详细展开。操作2依赖与操作1,操作3并不依赖与操作2,因此JVM很有可能将指令重排序为 1->3->2
。也就是说,当线程A执行赋值语句的时候,已经分配了内存并将引用指向了 INSTANCE
,此时线程B进入方法判断 INSTANCE
引用不为 null
,于是返回并使用(实际上对象并没有初始化完毕),导致出现问题。
在 J2SE 1.4
或更早的版本中使用双重检查锁有潜在的危险。
在 J2SE 5.0
中,这一问题被修正了。volatile
关键字可以保证内存可见性同时防止指令重排。
静态内部类实现(饿汉式改进) - 线程安全(thread-safe)
使用静态内部类封装,当 Singleton
类加载时,静态内部类 SingletonHolder 没有被加载进内存。只有当调用 getInstance
方法时才会被加载一次,延迟加载同时也是线程安全的。
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
Spring 中的应用
我们都知道Spring中Bean都是 '单例'(Singleton Scope)的,实际上这种'单例'跟我们所使用的单例模式(Singleton Pattern)是不同的,Spring Ioc容器会根据bean的定义创建实例并使用id绑定,因此一个Bean的定义实际上是可以产生多个不同的bean的,**Spring只保证每一个bean的id只有一个实例。而单例模式则是确保每一个类(ClassLoader)只有一个实例**。
@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
Object singletonObject = this.singletonObjects.get(beanName);
// 如果当前实例为null 并且正在创建中,走同步代码
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return singletonObject;
}
参考资料:
Q.E.D.