读源码学架构系列:单例模式

本来关于设计模式是打算在面向对象的设计原则写完之后写的,因为训练营有一道题是手写单例模式,于是干脆就先把单例模式总结一下,其它的后面再继续写。

什么是设计模式?

总的来说,设计模式是一种可重复使用的解决方案。我们用设计模式来解决面向对象设计时碰到的问题,而解决问题的目标就是让我们的设计满足面向对象的设计原则,以提升设计的可扩展性。

每一种设计模式都描述了一种问题的通用解决方案,且这种问题是反复地在我们的工作场景中出现的。

一个设计模式分为四个部分:

  • 模式的名称:由少量的字组成的名称,有助于我们表达我们的设计;
  • 待解决问题:描述了何时需要运用这种模式,以及运用模式的环境(上下文);
  • 解决方案:描述了组成设计的元素(类和对象)、它们的关系、职责以及合作。但这种解决方案是抽象的,它不代表具体的实现;
  • 结论:运用这种方案所带来的利弊。主要是指它对系统的弹性、扩展性和可移植性的影响;

这些设计模式按不同的分类方式可以分为不同的类型:

按功能来分可分为三类:

  • 创建模式:对类的实例化过程的抽象;
  • 结构模式:将类或者对象结合在一起形成更大的结构;
  • 行为模式:对在不同的对象之间划分责任和算法的抽象化;

按方式来分可分为两类:

  • 类模式:以继承的方式实现模式,静态的;
  • 对象模式:以组合的方式实现模式,动态的;

单例模式

一般,我们写单例,最容易写的是饿汉模式:

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

    private Singleton() {}

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

这种方式是没问题,但是挑剔的人会说,这里的INSTANCE在初始化的时候初始化了,如果这样的类非常多,但是,只有少数几个才使用,那这里实例化的对象就白白的占用了很多的内存。

这个时候,一般会想到Lazy了,也就是懒汉模式:

public class Singleton {
    private static Singleton instance = null;

    private Singleton() {}

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

这份代码看上去是懒汉模式,但是其实是有问题的,在多线程环境下,如果两个线程同时进入到if (instance == null)这里之后,instance会被初始化两次,两个线程拿到的不是同一个对象,这就不是单例了。

于是乎,首先想到的就是给getInstance()方法加上synchronized关键字:

public class Singleton {
    private static Singleton instance = null;

    private Singleton() {}

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

这份代码虽然是解决了重复实例化的问题,但是同步的粒度比较大,并发线程比较多的时候,每个线程调用都要去做同步的操作,而单例模式的实例对象,一旦被实例化之后就不会再改变(除非重启应用),所以这个同步的粒度是可以优化的。

于是乎,就出现了双重检查的懒汉模式:

public class Singleton {
    private static Singleton instance = null;

    private Singleton() {}

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

这里对instance的初始化做了两次检查,所以叫双重检查。双重检查完美的避免了上面的问题,只要instance被实例化了就不再走同步的代码块。

但是,上面的代码还是有问题的,问题点就在于instance的实例化。

Java中,实例化一个对象分为三步:

  1. 分配内存空间;
  2. 初始化对象;
  3. 将内存空间的地址赋值给对应的引用;

然而,现实是操作系统可以对指令进行重排序,所以上面的步骤可能会变成132,而不是123。所以,双重检查的懒汉模式需要给instance的定义处,加上volatile关键字,这个关键字在这里的作用是:禁止指令重排序优化。换句话说,就是volatile修饰的变量的赋值操作后面会有一个内存屏障,读操作不会被重排序到内存屏障之前。

所以,正确的代码应该是这样的:

public class Singleton {
    private volatile static Singleton instance = null;

    private Singleton() {}

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

双重检查(据说需要使用JDK 1.5+,在此之前JMM模型存在缺陷,我没有验证过)完成了,但是还是会有同步的操作存在,也就是说会有锁,而无锁肯定会优于无锁的方式,那有没有一种无锁的方式来实现这个单例呢?

答案是有的。

我最初看到这种方式实现的单例是在apache commons utils的一个类的源码中,第一次看到时我是真的惊讶到了,一个被大家写滥和鄙视到不行的单例模式,居然可以写得这么优雅。

具体代码如下,也是我交的作业1的答案:

public class Singleton {

    private Singleton() {}

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

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

}

这份代码没有锁,并且,LazyHolder中的INSTANCE实例在Singleton#getInstance()方法调用之前是不会被实例化的,这就不会出现饿汉模式那种过早占用内存的情况;而且,上面的代码是使用静态嵌套类的方式实现的,所以,能绝对的保证INSTANCE只会被实例化一次,因此,就不需要双重检查了。

至此,我个人觉得,最美的单例模式已经产生了~~

References:
  1. Software Design Pattern
  2. java 单例模式中双重检查锁定 volatile 的作用?
  3. 单例模式–双重检验锁真的线程安全吗