一、单例模式定义:(Spring 的bean默认类型,数据库连接池,线程池)
单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态,避免政出多头。
二、单例模式特点:
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。因此需要构造方法私有化,确保其他类不能实例化该类
3、单例类必须给所有其他对象提供这一实例。因此需要对外提供一个public方法获得该类的实例
单例模式保证了全局对象的唯一性,比如系统启动读取配置文件就需要单例保证配置的一致性。
三、单例模式的实现
1:饿汉式:牺牲空间换取时间,在类加载的时候就会创建类的实例,不管这个类是否会被调用。这样在一定程度上牺牲了空间,但是在需要的时候直接调用,又节省了一定时间。
/*
* 饿汉式 类创建的时候就创建实例实例且仅创建一次,可以避免进程的同步。 但是无法实现按需加载
*/
class SingletonA {
private static SingletonA instance = new SingletonA();
private SingletonA() {// 构造函数私有化。外部无法创建本类对象
}
public static SingletonA getInstance() {
return instance;
}
}
2:懒汉式:延迟加载,牺牲时间换取空间。
/*
* 懒汉式 可以按需加载,但是可能出现进程同步问题
*/
class SingletonB {
private static SingletonB instance;
private SingletonB() {
}
public static SingletonB getInstance() {
if (instance == null) {
instance = new SingletonB();
}
return instance;
}
}
3:改进懒汉式
因为饿汉式导致的问题,就产生了本篇文章的主题-双重检查锁定。一看到上述问题的产生,就会有人提出加上锁不就可以解决问题了吗,于是
class SingletonC {
private volatile static SingletonC instance;
private SingletonC() {
}
public synchronized static SingletonC getInstance() {
if (instance == null) {
instance = new SingletonC();
}
return instance;
}
}
但是大家都知道synchronized将导致性能开销(加锁,解锁,切换等)。于是又提出了第四个方式
4:双重检查(非安全)
// 双重检验锁
class SingletonC {
private static SingletonC instance;
private SingletonC() {
}
public static SingletonC getInstance() {
if (instance == null) {// 该句存在主要是因为被synchronized修饰的方法比一般方法要慢。多次调用内存消耗较大
synchronized (SingletonB.class) {// 解决线程的同步问题
if (instance == null) {
instance = new SingletonC();
}
}
}
return instance;
}
}
这样的解决办法看起来是两全其美的。首先当多个线程试图在同一时间创建对象的时候,会通过加锁来保证只有一个线程创建对象,其次在对象创建好后,执行getInstance()方法将不需要获取锁,直接返回已经创建好的对象,从而减少系统的性能开销。但这真的是安全的吗???
这就需要从jmm说起了。问题的根源出现在instance=new SingletonC()上。该代码功能是是创建实例对象,可以分解为如下伪代码步骤:
memory = allocate() ; //分配对象的内存空间
ctorInstance(memory); //②初始化对象
instance=memory; //③设置instance指向刚分配的内存地址
其中②和③之间,在某些编译器编译时,可能出现重排序(主要是为了代码优化),此时的代码如下:
memory = allocate() ; //分配对象的内存空间
instance=memory; //③设置instance指向刚分配的内存地址
ctorInstance(memory); //②初始化对象
单线程下执行时序图如下:
多线程下执行时序图:
由于单线程中遵守intra-thread semantics,从而能保证即使②和③交换顺序后其最终结果不变。但是当在多线程情况下,线程B将看到一个还没有被初始化的对象,此时将会出现问题。
解决方案:
1、不允许②和③进行重排序
2、允许②和③进行重排序,但排序之后,不允许其他线程看到。
5:改进式双重检查(基于volatile的安全解决方案)
对前面的双重锁实现的延迟初始化方案进行如下修改:
class SingletonC {
private volatile static SingletonC instance;
private SingletonC() {
}
public static SingletonC getInstance() {
if (instance == null) {// 该句存在主要是因为被synchronized修饰的方法比一般方法要慢。多次调用内存消耗较大
synchronized (SingletonB.class) {// 解决线程的同步问题
if (instance == null) {
instance = new SingletonC();
}
}
}
return instance;
}
}
使用volatile修饰instance之后,之前的②和③之间的重排序将在多线程环境下被禁止,从而保证了线程安全执行
6:静态内部类(基于类初始化的安全解决方案)
JVM在类的初始化阶段(即在Class被加载后,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化
class SingletonD {
private static class singleHolders {
public static SingletonD instance = new SingletonD();
}
private SingletonD() {
}
public static SingletonD getInstance() {
return singleHolders.instance;
}
}
假设两个线程并发执行getInstance(),下面是执行的示意图:
这个方案的实质是:允许“问题的根源”的三行伪代码中的2和3重排序,但不允许非构造线程(这里指线程B)“看到”这个重排序。
四:总结
延迟初始化降低了初始化类或创建实例的开销,但增加了访问被延迟初始化的字段的开销。在大多数时候,正常的初始化要优于延迟初始化。如果确实需要对实例字段使用线程安全的延迟初始化,请使用上面介绍的基于volatile的延迟初始化的方案;如果确实需要对静态字段使用线程安全的延迟初始化,请使用上面介绍的基于类初始化的方案。