您的位置:首页 > 其它

单例模式的几种实现方式总结

2016-12-13 23:24 519 查看
单例模式可能是设计模式中最容易理解,应用最广泛的模式。单例模式虽然简单,但其中的坑却不少,尤其是线程安全问题。本文主要对单例的各种写法做个总结,并分析其优缺点。

模式定义

保证一个类仅有一个实例,并提供一个访问它的全局访问点。

在Java中,单例模式的实现分两种,一种称为懒汉式,一种称为饿汉式,其实就是在具体创建对象实例的处理上,有不同的实现方式。

代码实现

1、懒汉式实现

当被问到要实现一个单例模式时,大多数人的第一反应是写出如下的代码:

/**
* 懒汉式(非线程安全)
*
* @author Ricky Fung
* @create 2016-12-13 22:37
*/
public class Singleton {
private static Singleton instance;
private Singleton(){

}
/**非线程安全*/
public static Singleton getInstance(){
if(instance==null){
instance = new Singleton();
}
return instance;
}
}


这段代码简单明了,但是却存在致命的问题。当有多个线程并行调用 getInstance() 的时候,就会创建多个实例。也就是说在多线程下不能正常工作。

为了解决线程安全问题,最简单的方法是将整个 getInstance() 方法设为同步(synchronized),代码如下:

/**
* 懒汉式(线程安全)
*
* @author Ricky Fung
* @create 2016-12-13 22:37
*/
public class Singleton {
private static Singleton instance;
private Singleton(){

}

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


上述做法虽然解决了线程安全问题,但是它并不高效,因为在任何时候只能有一个线程调用 getInstance() 方法。

改进的办法就是:在真正需要同步的地方才加锁,即第一次创建单例实例对象时。使用 双重检查加锁(double-check)。

双重检查加锁(double checked locking pattern)是一种使用同步块加锁的方法。程序员称其为双重检查锁,因为会有两次检查 instance == null,一次是在同步块外,一次是在同步块内。代码如下:

public class Singleton {
private static Singleton instance;
private Singleton(){

}
/**双重检查(double-check)**/
public static Singleton getInstance(){  //1
if(instance==null){  //2
synchronized (Singleton.class){  //3
if(instance==null){  //4
instance = new Singleton(); //5
}
}
}
return instance;
}
}


如上面代码所示,如果第一次检查instance不为null,那么就不需要执行下面的加锁和初始化操作。因此可以大幅降低synchronized带来的性能开销。上面代码表面上看起来,似乎两全其美:

双重检查锁定看起来似乎很完美,但这是一个错误的优化!在线程执行到第2行代码读取到instance不为null时,instance引用的对象有可能还没有完成初始化。

问题的根源

前面的双重检查锁定示例代码的第7行(instance = new Singleton();)创建一个对象。这一行代码可以分解为如下的三行伪代码:

memory = allocate();   //1:分配对象的内存空间
ctorInstance(memory);  //2:初始化对象
instance = memory;     //3:设置instance指向刚分配的内存地址


但是在 JVM 的即时编译器(JIT)中存在指令重排序的优化。上面三行伪代码中的2和3之间,可能会被重排序,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化)导致最终出错。

解决办法:只需要将 instance 变量声明成 volatile 就可以了,禁止指令重排序优化。

/**
* 懒汉式(线程安全)
*
* @author Ricky Fung
* @create 2016-12-13 22:37
*/
public class Singleton {
private volatile static Singleton instance;
private Singleton(){

}
/**双重检查(double-check)**/
public static Singleton getInstance(){
if(instance==null){
synchronized (Singleton.class){
if(instance==null){
instance = new Singleton();
}
}
}
return instance;
}
}


注意

在Java 1.5之前版本中,很多JVM对于volatile

关键字的实现有问题,会导致双重检查加锁的失败,因此双重检查加锁的机制只能用在Java 5及以上的版本。

2、饿汉式

这种方法非常简单,因为单例的实例被声明成 static 和 final 变量了,在第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的。

代码如下:

/**
* 饿汉式(线程安全)
*
* @author Ricky Fung
* @create 2016-12-13 22:37
*/
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton(){

}

public static Singleton getInstance(){
return INSTANCE;
}
}


更高效、安全的实现方式

1、静态内部类

这个方案被称为 Lazy initialization holder class模式,代码如下:

/**
* 懒汉式(线程安全)
*
* @author Ricky Fung
* @create 2016-12-13 22:37
*/
public class Singleton {

private Singleton(){
}

public static Singleton getInstance(){
return SingletonHolder.INSTANCE;
}

private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
}


这种实现方式由JVM来保证线程的安全性,另外,由于是在静态内部类SingletonHolder里面去创建对象,只要不使用这个静态内部类,就不会创建对象实例 ,因此它属于懒汉式;

2、枚举

按照《Effective Java 第二版》中的说法:单元素的枚举类型已经成为实现Singleton的最佳方法。

/**
* 枚举实现单例
*
* @author Ricky Fung
* @create 2016-12-13 22:39
*/
public enum Singleton {
INSTANCE;

public void doSth(){
//do stuff
}
}


使用枚举来实现单例会更加简洁,而且无偿提供了序列化的机制,并由JVM从根本上提供保障,绝对防止多次实例化,是更简洁、高效、安全的实现单例的方式。

参考

双重检查锁定与延迟初始化:http://www.infoq.com/cn/articles/double-checked-locking-with-delay-initialization
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息