5种JAVA单例模式的实现、原理和演化

列举JAVA实现单例模式的五种方法,以及每种方法的优缺点和实现的细节原理。

作者 jooop 日期 2017-04-07
5种JAVA单例模式的实现、原理和演化

写在前面:

最近一直在深入学习并理解之前学过的JAVA 集合、多线程、MySQL、框架原理等相关内容基础的原理和实现,以及对这些知识的总结归纳,不过在深入的过程发现许多自认为掌握了的东西,其实在实现的细节和原理方面的掌握还是有些不足。
许多东西第一次看的时候理解了,也自认为掌握了,但过段时间回忆起来还是有些出入,而想要再次查阅书籍、他人博客的时候不免要经过一些其他信息的干扰,故而觉得还是很有必要根据自己的理解整理、将自己的见解记录下来,既方便自己、可能也会对他人有所帮助。

一、单例模式三要素

  • 私有的构造方法;
  • 指向自己实例的私有静态引用;
  • 以自己实例为返回值的静态的公有方法。

二、懒汉式(实现方式的演化)

之所以用2个版本依次推进来讲解懒汉式的实现,是希望能够在学习的过程中对其具体实现的细节和原理可以有更深入、深刻的理解。
所谓懒汉式,即在需要调用单例的对象时才进行对象创建。

1、线程不安全版本

public class Singleton{
private Singleton(){}
private static Singleton instance;
public static Singleton getInstance(){
if(instance==null){
instance = new Singleton();
}
return instance;
}
}

线程不安全的原因:
会出现一种情况:首先A线程执行if(instance==null)发现没有对象存在,于是进入if中,然而在还未执行instance = new Singleton()时线程被挂起,此时B线程也执行并进入if中,创建了对象,再轮到A线程执行时会再次创建另外一个对象。此时导致单例失效。

测试

测试用例:

public class Main implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+":"+Singleton.getInstance());
}
public static void main(String[] args) {
for(int i=0;i<2;i++){
Main test = new Main();
new Thread(test,"线程"+i).start();
}
}
}

线程不安全输出结果:

线程0:Singleton.Singleton@1dda492b
线程1:Singleton.Singleton@7fc089c2

随手试了下,线程不安全的情况挺容易发生的,(没有重写toString方法,因此直接输出的hashCode),可以看到两个线程得到的对象并不是同一个。

2、同步锁synchrodized保证线程安全、但效率低

public class Singleton{
private Singleton(){}
private static Singleton instance;
public static synchronized Singleton getInstance(){
if(instance==null){
instance = new Singleton();
}
return instance;
}
}

此版本在获取对象的方法上使用了同步锁来保证了线程安全,但是效率较低。因为同步操作只有在第一次调用时才需要,但是在getInstance()方法上加锁以后导致每次获取该对象都要进行同步处理,此时其他线程需要等待,所以效率低。

三、双重检验锁

对懒汉式的线程安全版本进行优化(非线程安全)

双重检验锁其实也是基于懒汉式的,是对其线程安全版本的优化处理。

public class Singleton{
private Singleton(){}
private static Singleton instance;
public static Singleton getInstance(){
if(instance==null){
synchronized(Singleton.class){
if(instance==null){
instance = new Singleton();
}
}
}
return instance;
}
}

双重检验锁和直接使用synchronized来锁住getInstance方法的懒汉式相比,主要优化之处在于减小了锁的粒度。
第一重校验的作用:先判断实例有没有创建,如没有创建才进行同步代码块的执行去创建对象。这样保证了之后的使用的效率。
第二重校验的作用:在同步代码块内部再次进行对象是否已经创建的校验主要是为了防止多个线程进入第一个if中,而后都会执行同步代码块导致的重复创建对象的情况。

JVM指令重排序导致的潜在的问题和解决(线程安全)

上面的优化版本虽然看起来很好地同时解决了线程安全问题和效率问题,但其并不是线程安全的
问题:主要问题在于JVM的“优化指令重排”机制。以及instance = new Singleton();并非原子性操作。
instance = new Singleton();的过程如下:
1、在堆上开辟一块内存空间
2、对对象进行设置、初始化
3、返回对象引用到栈中
而在JVM中的优化指令重排序可能会导致其原本的1、2、3执行顺序变为1、3、2。此时若当A线程执行了1、3挂起后,B线程调用了getInstance(),会判断instance!=null,从而将实际上未初始化完成的instance返回,导致调用该对象出错。
解决:自JDK1.5之后的关键字volatile会给所修饰的对象有两个特性:1、确保所有线程看到这个变量的值是一致。2、禁止优化指令重排序。
此处主要用到禁止优化指令重排的效果,其原理是在字节码层面上,在volatile修饰的变量的赋值语句后面会有一个内存屏障,读操作不会被优化重排到内存屏障之前,内次不会发生执行顺序变成1、3、2的情况。

public class Singleton{
private Singleton(){}
private static volatile Singleton instance;
public static Singleton getInstance(){
if(instance==null){
synchronized(Singleton.class){
if(instance==null){
instance = new Singleton;
}
}
}
return instance;
}
}

四、饿汉式

public class Singleton{
private Singleton(){}
private static Singleton instance = new Singleton();
public static Singleton getInstance(){
return instance
}
}

饿汉式和懒汉式相比主要区别在于单例对象在类加载时就被初始化,即使没有调用过getInstance方法。因此即使不加锁也不存在线程安全问题,是天生线程安全的。
不过饿汉式并不能适应所有的使用情况,当Singleton创建的对象需要依赖参数或者配置文件,在创建instance对象前需要调用某个方法对其设置参数的情况无法实现。

五、静态内部类实现

public class Singleton{
private Singleton(){}
private static class SingletonHolder{
private static Singleton instance = new Singleton();
}
public static Singleton getInstance(){
return SingletonHolder.instance;
}
}

静态内部类实际上也是懒汉式的一种实现方式。其线程安全,没有性能缺陷,且不依赖JDK版本。
原理:静态内部类只有在调用getInstance()方法时才加载,从而实例化instance,而且只加载一次。

六、枚举实现

public enum Singleton{
INSTANCE;
public void whateverMethod(){}
}

枚举实现方法是最简单的一种,也是《effective java》中推荐的一种写法,其原理大致是因为JDK1.5之后的枚举数据都是线程安全的。
不过个人对枚举类使用和了解都不够深入, 其具体细节和原理有待以后补充。