vlambda博客
学习文章列表

你真的了解单例模式吗?


单例Singleton

今天我们来聊聊java中应用非常广泛的单例模式,首先你可以在脑海中想一想我们在平常的开发中哪些地方用到了单例,是怎样实现的,如果让你实现一个单例你有哪些方法呢?

用途

确保一个类只有一个实例,并提供对其的全局访问点。从而保证数据内容的一致性,节省内存资源。

例如,Windows 中只能打开一个任务管理器,这样可以避免因打开多个任务管理器窗口而出现各个窗口显示内容不一致的情况,或造成内存资源的浪费。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。

在计算机系统中,多线程中的线程池、显卡的驱动程序对象、打印机的后台处理服务、应用程序的日志对象、数据库的连接池、网站的计数器、Web 应用的配置对象、应用程序中的对话框、系统中的缓存等常被设计成单例。这些应用都或多或少具有资源管理器的功能。总之,选择单例模式就是为了避免不一致状态。

定义

单例(Singleton)模式的定义:指一个类只有一个实例,且该类能自行创建这个实例的一种模式。

特点

  1. 单例类只有一个实例对象;

  2. 该单例对象必须由单例类自行创建;

  3. 单例类对外提供一个访问该单例的全局访问点;

代码实现

让类的构造函数私有,在类内创建一个静态对象,并创建一个公有的静态方法访问这个对象。

写法上有2种方式:

[1]立即加载方式, 也叫“饿汉模式”

单例在类加载初始化时就创建好一个静态的对象供外部使用,除非系统重启,否则这个对象不会改变,不同线程来调用getInstance()的时候,获取的都是类初始化就创建的同一个实例,所以本身就是线程安全的。

//饿汉式单例类.在类初始化时,已经自行实例化(线程安全)
public class Singleton {

    private static Singleton ss = new Singleton();

    private Singleton() {}

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

唯一的缺点就是实例创建过早,类初始化就创建了,还没调用就已经存在了,容易造成内存资源浪费。

[2]延迟加载方式, 也叫“懒汉模式”

饿汉是类一旦加载,就把单例初始化完成,保证调用getInstance()的时候,单例是已经存在的;

而懒汉比较懒,只有当调用getInstance()的时候,才会去初始化这个单例。

//懒汉式单例类.在第一次调用的时候实例化自己(非线程安全)
public class LazySingleton {

    private static LazySingleton ls = null;

    private LazySingleton({}

    public static LazySingleton getInstance({
        if (ls == null) {
            ls = new LazySingleton();
        }
        return ls;
    }
}

但是这种懒汉式单例的实现没有考虑线程安全问题,它是线程不安全的。

为什么说这种代码写法线程不安全呢?

假设LazySingleton类刚刚被初始化,ls对象还是空,这时候两个线程A和B同时访问getInstance方法:

因为ls是null,所以A和B两个线程同时通过了条件判断,开始执行new操作,显然ls被构建了两次。

要保证懒汉式的线程安全,有下面3种方法。

(1)使getInstance()同步

//懒汉式单例类.在第一次调用的时候实例化自己(线程安全 -- 同步法)
public class LazySingleton {

    private static volatile LazySingleton ls = null//保证ls在所有线程中同步

    private LazySingleton() {}

    public static synchronized LazySingleton getInstance() {//getInstance 方法前加同步
        if (ls == null) {
            ls = new LazySingleton();
        }
        return ls;
    }
}

在方法调用上加了同步,虽然线程安全了,但是每次都要同步,会影响性能,毕竟大多数情况下是不需要同步的;

(2)双重检查锁定DCL(double checked locking)

//懒汉式单例类.在第一次调用的时候实例化自己(非绝对线程安全[取决于编译器] -- 双重检查锁定)
public class LazySingleton {

    private static LazySingleton ls = null;

    private LazySingleton() {}

    public static LazySingleton getInstance() {
        if (ls == null) {//双重检查
               synchronized (LazySingleton.class) {  //锁住整个类  
                if (ls == null) { //双重检查
                    ls = new LazySingleton();   
                }    
            } 
        }
        return ls;
    }
}

在getInstance中做了两次null检查,第一次是为了提高运行效率;第二次是为了进行同步,避免多线程问题,进入synchronized临界区以后,还要再做一次判空。因为当两个线程同时访问的时候,线程A构建完对象,线程B也已经通过了第一次的判空验证,不做第二次判空的话,线程B还是会再次构建ls对象。

虽然上面这样写逻辑上看没什么问题,但是和编译器有关,不算绝对的线程安全。

以java为例,需要了解JVM编译器的指令重排

当AB两线程同时调用getInstance,A执行到new语句,B准备执行判断。

什么叫指令重排呢,比如A在执行语句ls = new LazySingleton(),看起来是一句话,但这并不是一个原子操作(要么全部执行完,要么全部不执行,不能执行一半),这句话被编译成8条汇编指令,大致做了3件事情:

  1. 给LazySingleton的实例分配内存。

  2. 初始化LazySingleton的构造器

  3. 将LazySingleton对象指向分配的内存空间(注意到这步ls就非null了)。

由于Java编译器允许处理器乱序执行(out-of-order),以及JDK1.5之前JMM(Java Memory Model)中Cache、寄存器到主内存回写顺序的规定,上面的第二点和第三点的顺序是无法保证的,也就是说,执行顺序可能是1-2-3也可能是1-3-2,如果是1-3-2,并且在3执行完毕、2未执行之前,被切换到线程B上,这时候LazySingleton因为已经在线程A内执行过了第三点,ls已经是非空了,所以线程B直接拿走ls,然后使用,然后顺理成章地报错,而且这种难以跟踪难以重现的错误估计调试上一星期都未必能找得出来。

DCL的写法来实现单例是很多技术书、教科书(包括基于JDK1.4以前版本的书籍)上推荐的写法,实际上是不完全正确的。的确在一些语言(譬如C语言)上DCL是可行的,取决于是否能保证2、3步的顺序。在JDK1.5之后,官方已经注意到这种问题,因此调整了JMM、具体化了volatile关键字,因此如果JDK是1.5或之后的版本,只需要将ls的定义加上volatile关键字,就可以禁止编译器重排序,就可以使用DCL的写法来完成单例模式。所以JDK1.5以后可以加上volatile来实现DCL方式的绝对线程安全。

//懒汉式单例类.在第一次调用的时候实例化自己(线程安全 -- 双重检查锁定)
public class LazySingleton {

    private static volatile LazySingleton ls = null//volatile禁止指令重排

    private LazySingleton() {}

    public static LazySingleton getInstance() {
        if (ls == null) {//双重检查
               synchronized (LazySingleton.class) {  //锁住整个类  
                if (ls == null) { //双重检查
                    ls = new LazySingleton();   
                }    
            } 
        }
        return ls;
    }
}

DCL方式要小心使用,需要了解具体的语言编译器,在禁止编译器指令重排后,才能保证绝对线程安全。

(3)静态内部类

//懒汉式单例类.在第一次调用的时候实例化自己(线程安全 -- 静态内部类)
public class LazySingleton {

    private LazySingleton() {}

    public static LazySingleton getInstance() {
        return InnerClass.ls;
    }

    private static class InnerClass {
        private static final LazySingleton ls = new LazySingleton();
    }
}

利用了classloader的机制来保证初始化单例时只有一个线程,所以也是线程安全的。

破坏单例模式

1. 反射

以最简单而且线程安全的"饿汉模式"来进行测试。

//获得构造器
Constructor con = Singleton.class.getDeclaredConstructor();
//设置为可访问
con.setAccessible(true);
//构造两个不同的对象
Singleton singletonA = (Singleton)con.newInstance();
Singleton singletonB = (Singleton)con.newInstance();
//验证是否是相同对象
System.out.println(singletonA.equals(singletonB));

最后的比较结果是false,反射可以访问类里面所有的私有属性和方法。所以反射访问私有构造器是可以非常容易的创建多个对象实例,从而破坏单例模式。简单的处理方法就是在私有构造器里面进行判断,禁止进行反射,但是也仅限于饿汉的写法,懒汉的还是不能避免。

//饿汉式单例类.线程安全.避免反射创建
public class Singleton {

    private static Singleton ss = new Singleton();

    //避免反射和多类加载器破坏
    private Singleton({
        if (Singleton.ss != null) {
            throw new Exception("Singleton can not use Reflection");
        }
    }

    public static Singleton getInstance({
        return ss;
    }
}

2.序列化和反序列化

还以最简单而且线程安全的"饿汉模式"写法来进行测试。

Singleton instanceA = Singleton.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new          FileOutputStream("sersingle_file"));
oos.writeObject(instanceA);

File file = new File("sersingle_file");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Singleton instanceB = (Singleton) ois.readObject();
System.out.println(singletonA.equals(singletonB));

最后的比较结果是false,说明两个对象不一样,所以序列化和反序列化也破坏单例模式。解决方法是在单例中添加readResolve方法。

//饿汉式单例类.避免反序列破坏
public class Singleton implements Serializable{

    private static Singleton ss = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return ss;
    }

    //避免反序列破坏
    protected Object readResolve() {
        return ss;
    }
}

最佳实践

枚举

public enum EnumSingleton {
  INSTANCE;
}

EnumSingleton enumSingletonA = EnumSingleton.INSTANCE;
EnumSingleton enumSingletonB = EnumSingleton.INSTANCE;
assertEquals(enumSingletonA, enumSingletonB); // true

Joshua Bloch, Effective Java 2nd Edition p.18

A single-element enum type is the best way to implement a singleton

单元素枚举类型是实现单例的最佳方法

有了enum类型修饰,JVM会阻止反射强行构造对象,而且可以在对象被反序列化的时候,保证反序列结果返回同一对象,并且是线程安全的。唯一的不足就是不是延迟加载,单例对象是在枚举类被初始化加载的时候就进行创建了。


JAVA中单例的应用案例

http://docs.oracle.com/javase/8/docs/api/java/lang/Runtime.html#getRuntime()

http://docs.oracle.com/javase/8/docs/api/java/awt/Desktop.html#getDesktop()

http://docs.oracle.com/javase/8/docs/api/java/lang/System.html#getSecurityManager()