解刨单例模式

仅供学习交流,如有错误请指出,如要转载请加上出处,谢谢

单例模式,也叫单子模式,是一种常用的软件设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。——来自wikipedia

单例模式大致上分为两种模式,饿汉模式和懒汉模式,在开发环境中有很多的应用,比如Spring的bean工厂就应用了单例模式来对bean进行初始化,他对类的实例进行了统一的管理,每次返回该类的唯一实例,也优化了实例化类的资源利用。

饿汉模式

1
2
3
4
5
6
7
8
9
public class HungrySingleton {
private final static HungrySingleton INSTANCE = new HungrySingleton();
private HungrySingleton() {}
public static HungrySingleton getInstance() {
return INSTANCE;
}
}

饿汉模式指在类的实例在全局定义,利用static和final修饰,保持类的唯一性,在类装载的时候就初始化了, 因为创建实例本身是线程安全的,所以饿汉模式也是线程安全的。

但是饿汉模式的应用场景是明确类本身实例的信息,因为这是在类装载前就实例化了,无法改变,但是如果想根据上下文或者所依赖参数的变化来动态的实例化类,饿汉模式就不匹配了,于是另一种懒汉模式就登场了。

懒汉模式

懒汉模式指全局的实例在第一次调用的时候才加载

简单模式

1
2
3
4
5
6
7
8
9
10
11
12
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton (){}
public static LazySingleton getInstance() {
if (instance == null) {// 线程1
instance = new LazySingleton();// 线程2
}
return instance;
}
}

这是一个很简单的懒汉模式,在单线程的环境下,通过第一次检查instance是否为空来获取唯一的实例,但是在多线程的环境下,由于instance = new LazySingleton()不是一个原子性的操作,会受到其他线程的干扰,如上所示:

  1. 如果线程1在if (instance == null) {挂起,线程2在instance = new Singleton()开始执行,instance指向了一个内存空间,但是还没有开始初始化对象( 指令重排序,下面会讲到 ),线程2挂起,线程1这个时候继续,这个时候判断instance不为null,返回的只是一个空内存块(没有实例化对象),很容易造成NullPointException,

  2. 如果线程2在instance = new Singleton()挂起,线程1在if (instance == null) {开始执行,这个时候instance还没有指向内存,instance为null,也进来进行了创建实例的步骤,线程1和线程2创建了两个实例,违背了单例的思想

所以以上两种情况表明这种简单模式是线程不安全的

单重检验锁模式(single checked locking pattern)

1
2
3
4
5
6
7
8
9
10
11
12
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton (){}
public static synchronized LazySingleton getInstance() {
if (instance == null) {// single check
instance = new LazySingleton();
}
return instance;
}
}

以上的模式是在获取实例的方法getInstance()加上同步锁synchronized来修饰,以保证线程的安全性,但是虽然保证了线程安全,但是这种暴力的同步严重影响了程序执行的性能,在执行getInstance()方法时,频繁的线程的更换调度,对于性能是一个很大的开销。

如果instance已经实例化了,对于上述模式来讲,他还是要等待前面的线程获取完实例才能获取实例,这样实现很低效,其实同步只是针对实例化对象的过程,对于已经实例化对象的instance来说,只需要返回就可以了,不需要同步。

双重检验锁模式(double checked locking pattern)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton (){}
public static LazySingleton getInstance() {
if (instance == null) {//single check 线程1
synchronized (LazySingleton.class) {
if (instance == null) {//double check
instance = new LazySingleton(); //线程2
}
}
}
return instance;
}
}

双重检验锁模式又对单重检验锁模式进行了优化,他用两次检查来判断instance是否被实例化,同步锁只是针对instance的实例化,对于instance已经实例化的情况下,直接返回instance,不进入同步锁的代码块,大大的提高了性能

但是,又重现了简单模式的第一种情况,如上代码所示,假设线程2在实例化对象只是在instance指向了内存空间,但是还没有实例化对象(指令重排序),这个时候线程1的instance!=null,直接返回instance,造成NullPointException,现在问题来了,什么是指令重排序?

一般的情况下,程序运行代码是顺序运行的,但是会存在一些指令的重排序问题,比如

1
2
3
int a=1
int b=1
int c=a+b;

以上代码使用指令来执行,分为以下5个步骤:

  1. 对a赋值1
  2. 对b赋值1
  3. 获取a的值
  4. 获取b的值
  5. 运算a+b的值存在c的内存中

上述的五个步骤有时并不是按照顺序进行的,有时你执行步骤1对a赋值1时,就会执行步骤3获取a的值,因为他们存在数据依赖,这就是发生了指令重排(具体了解,访问 http://tech.meituan.com/java-memory-reordering.html ),对于实例化对象来讲,具体的步骤如下:

  1. 分配内存
  2. 实例化对象
  3. 引用指向内存对象

正常的情况下只有实例化了对象才会引用指向内存对象,但是如果这时发生了指令重排序,执行顺序变成了1,3,2,在执行步骤3还没有执行步骤2就执行线程1了,这个时候就会造成异常错误,这个时候就会使用volatile关键字来避免指令重排序。

volatile式模式(double checked locking pattern with )

volatile的定义是:java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排他锁单独获得这个变量。Java语言提供了volatile,在某些情况下比锁更加方便。如果一个字段被声明成volatile,java线程内存模型确保所有线程看到这个变量的值是一致的。

正如上所说,java线程内存模型确保所有线程看到这个变量的值是一致的就是volatile的可见性,正是因为他的可见性,要求所有线程看见该变量要一致,所以代表着volatile的另一个特性:禁止指令重排序,其实,在JDK1.5之前volatile是不能保证能够禁止指令重排序的,在JDK1.5之后才能应用于双重检查模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class LazySingleton {
private volatile static LazySingleton instance;
private LazySingleton (){}
public static LazySingleton getInstance() {
if (instance == null) {//single check
synchronized(LazySingleton.class){
if (instance == null) {//double check
instance = new LazySingleton();
}
}
}
}
return instance;
}
}

静态内部类模式(static nested class pattern)

静态内部类模式也是一种懒汉模式,他利用内部类的特性(调用时才加载)来创建实例

1
2
3
4
5
6
7
8
9
public class LazySingleton {
private static class SingletonClass {
private static final LazySingleton INSTANCE = new LazySingleton();
}
private LazySingleton (){}
public static final LazySingleton getInstance() {
return SingletonClass.INSTANCE;
}
}

这种模式也能保证线程的安全性,JVM在保持类的信息的一致性,加载类的时候是线程安全的,而且不需要同步来执行getInstance()方法。

参考

https://zh.wikipedia.org/wiki/%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F
http://tech.meituan.com/java-memory-reordering.html