简介:本文将介绍几种单例模式的几种写法,并分析其在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的 方法同步 会造成额外的开销,因此有以下优化方式:

  1. 检查变量是否被初始化(不去获得锁),如果已被初始化立即返回这个变量。
  2. 获取锁
  3. 第二次检查变量是否已经被初始化:如果其他线程曾获取过锁,那么变量已被初始化,返回初始化的变量。
  4. 否则,初始化并返回变量。

Java中,这看上去是一种比较有效的解决方案,然而实际上还需要考虑一些细微的问题。INSTANCE = new Singleton()并不是一个原子操作,其实际上可以抽象为下面几条JVM指令:

  1. memory =allocate(); //1:分配对象的内存空间
  2. ctorInstance(memory); //2:初始化对象
  3. 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.