单例模式是什么?

单例模式Singleton)通常来讲,就是让一个类仅仅只有一个实例,并提供一个全局访问点。

如何控制让一个类只能有一个实例对象呢?全局变量?不行,虽然全局变量可以保证一个对象被访问,但是还是无法去阻止你实例化多个对象。
既然外部无法进行控制,那就交给类自己维护吧,而外部仅仅只是起一个“通知的作用”,由类自己负责保存它的唯一实例,并对外提供一个访问该实例的方法。

单例模式有三个特点:

  1. 单例类由且仅有一个实例。
  2. 单例类必须自己创建与保存这个唯一实例。
  3. 单例类必须向外部提供访问这一实例的方法。

为什么要用单例模式?

一个显而易见的好处就是减少资源浪费,因为仅仅只有一个实例被创建,大大节约了资源。
比如我们要加载的一些配置,这些配置存在于整个公共周期,且允许被公共访问,所以只需要维持一份即可。

一个典型的应用如网站在线人数的统计,如何保证所有人实时访问得到的在线人数是一致的呢?最好的办法就是让所有人公共的访问唯一的实例人数,这时单例模式就起到了作用。

还有很多比如数据库连接池的设计,日志的管理,windows的文件管理器等等。

如果出现线程同步并发访问的问题,且该类只需一个实例面向外部访问,那么就要考虑用单例模式。

单例模式的实现

饿汉式实现

class Singleton {
    /**
     * private限定外部访问,而static保证只存在一个该实例。
     * 在初始化的时候就创建实例
     */
    private static Singleton instance = new Singleton();

    /**
     * 构造方法使用private限定外部实例化该类
     */
    private Singleton(){}

    /**
     * 实例的全局访问点
     * @return 唯一实例
     */
    public static Singleton getInstance() {
       return instance;
    }

}

我们访问该实例只需要通过全局访问点即可:

Singleton singleton = Singleton.getInstance();

这种实现方法称作饿汉式单例模式
顾名思义,这种方法在类初始化的时候实例就被创建出来,就像饿了很久的汉子一样,见到一点肉就着急忙慌的抢来吞(笑)。

显而易见的,这种单例模式是线程安全的,因为return是原子性操作,静态实例的初始化又只进行一次。
但是很容易想到的一个问题就是,如果加载该对象耗时很长而又长时间不使用的话,岂不是性能浪费过大
而为了解决这个问题,有了下面的懒汉式实现方法

懒汉式实现

class Singleton {
    /**
     * private限定外部访问,而static保证只存在一个该实例。
     */
    private static Singleton instance;

    /**
     * 构造方法使用private限定外部实例化该类
     */
    private Singleton(){}

    /**
     * 实例的全局访问点
     * @return 唯一实例
     */
    public static Singleton getInstance() {
        /* 如果没有实例化就实例化,否则返回已有实例 */
        if (instance == null) {
            instance = new Singleton();
        }

        return instance;
    }

}

懒汉式单例模式只有在第一次调用的时候才会进行实例化
我不调用,它就不去创建实例,这样性能问题就解决了。

可是,有得必有失,这种单例模式不是线程安全的!
注意实例化的代码:

if (instance == null) {
    instance = new Singleton();
}

当有多个线程同时访问的时候,会使得其创建多个实例。

没办法,出现问题,解决问题,为了让它变成线程安全的,我们考虑对其进行加锁处理

多线程下的单例模式

单重锁定

class Singleton {
    /**
     * private限定外部访问,而static保证只存在一个该实例。
     */
    private static Singleton instance;

    /**
     * 建立一个静态对象锁
     */
    private static final Object syncRoot = new Object();

    /**
     * 构造方法使用private限定外部实例化该类
     */
    private Singleton(){}

    /**
     * 实例的全局访问点
     * @return 唯一实例
     */
    public static Singleton getInstance() {
        /* 如果没有实例化就实例化,否则返回已有实例 */
        synchronized(syncRoot) {
            if (instance == null) {
                instance = new Singleton();
            }
        }

        return instance;
    }

}

这样虽然可以解决线程不安全的问题,但是每次调用方法都需要synchronized,这对性能的消耗也是比较大的。
我们可以再对这种模式优化一下。

双重锁定

class Singleton {
    /**
     * private限定外部访问,而static保证只存在一个该实例。
     */
    private volatile static Singleton instance;

    /**
     * 建立一个静态对象锁
     */
    private static final Object syncRoot = new Object();

    /**
     * 构造方法使用private限定外部实例化该类
     */
    private Singleton(){}

    /**
     * 实例的全局访问点
     * @return 唯一实例
     */
    public static Singleton getInstance() {
        /* 如果没有实例化就实例化,否则返回已有实例 */
        if (instance == null) {
            /* 先判断是否存在,不存在再进行加锁处理 */
            synchronized (syncRoot) {
                if(instance == null) {
                    instance = new Singleton();
                }
            }
        }

        return instance;
    }

}

相比较之前有什么不同呢?会发现有两个点发生了变动:

  1. 先判断是否存在,再进行加锁处理。
    这样如果存在就直接返回,不必再进入同步代码块,大大提高了性能。
    也许你会疑问为什么在同步代码块里要判断第二次?
    很简单,如果多个线程同时经过第一次判断后等待,没有第二次判断的话,岂不是又要创建很多个实例了?
    这样既保证了线程安全,又对性能没有太大的损耗,一举两得~
  2. 在类属性上加了volatile关键字。
    简单解释一下volatile关键字,volatile保证了共享变量的可见性,也就是说当一个线程修改一个共享变量时,另一个线程能读到这个修改的值。
    同时,与volatile变量有关的所有操作将禁止指令重排序的进行。
    (重排序是为了优化程序性能对指令序列进行排序的一种手段)

那么为什么要加volatile关键字呢
我们首先来把instance = new Singleton();分解一下,这一步其实可以分解为几个步骤:

  1. 为该对象实例分配内存区域。
  2. 对实例数据进行初始化(不包括静态数据,静态数据在类加载的时候就已经初始化了)。
  3. 调用构造函数。
  4. 设置instance指向刚分配的地址。

考虑一种情况,就是如果第3、4步发生了指令重排序的话,也就是说先让instance引用指向刚分配的地址,再调用构造函数
假设有一个线程A和一个线程B,线程A执行完了第3步(为引用分配地址),时间片结束,线程B执行if (instance == null)进行判断,发现引用不为null,就将引用直接返回了,可是这个时候的引用还没有调用构造函数完成初始化,这就是问题所在。

静态内部类实现

单例模式一种推荐的写法是:

class Singleton {

    /**
     * 构造方法使用private限定外部实例化该类
     */
    private Singleton(){}

    /**
     * 静态内部私有类
     * 在第一次加载时初始化并创建实例
     */
    private static class SingletonHolder {
        private static final Singleton instance = new Singleton();
    }

    /**
     * 实例的全局访问点
     * @return 唯一实例
     */
    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }

}

因为第一次加载(调用getInstance())才会对内部类进行初始化,所以其实现了延迟加载。
而又因为静态变量只初始化一次,所以其是线程安全的。
这种方法兼并了较高的性能以及线程安全的优点,所以是推荐使用的一种方法。

总结

单例模式主要用于解决数据不一致和性能消耗过大的问题。
它让类自己持有自己的唯一实例,并对外开放一个全局访问点。

单例模式有很多种方法实现,而用哪种方法,还是要根据使用环境来决定。
比如在多线程并发的环境下,我们当然要选线程安全的模式来执行。
而如果实例创建耗费资源较少,耗时短的话,饿汉式单例模式也不妨是一种不错的选择。