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

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

模式定义

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

在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

你可能感兴趣的:(Java)