单例模式

单例模式是设计模式中最简单的设计模式之一。他和工厂模式同属于创建型模式,都用于类的实例化。不过两者的区别很大,要解决的问题也不一样。

单例模式保证一个类只会被实例化一次,使用的时候通过单例提供的方法来获取实例。在确保线程安全的前提下,很多时候我们只需要同一个类的一个实例即可,而不是在任何使用的地方都实例化一个新对象。新对象创建是有成本的,不但要花时间,而且占用内存。另外有的时候我们需要一个全局唯一的实例,比如计数器,全局多个计数器就会计数混乱不准确,如下图所示。单例模式就是为了实现全局一个实例的需求。
图片描述

1. 实现单例模

实现单例模式,其实我们需要实现如下需求:

  1. 提供获取实例的方法。此方法会控制全局仅有一个实例,而不会重复创建实例;
  2. 全局唯一的实例要有地方能存放起来;
  3. 不能随意通过new关键字创建实例。这样才能控制调用方只能用受控的方法来创建对象。

针对以上三点需求我们需要做如下事情:

  1. 编写一个获取实例的公有方法,已经创建过实例就直接返回实例,否则进行实例化;
  2. 实例化好的对象存哪里呢?存在类当中是最好的。这样不用引入新的类,而且也符合就近原则;
  3. 禁止通过new关键字初始化,只需要把无参构造方法私有化。此外不要添加任何有参数的构造方法。

我们按照上面的思路实现第一版单例模式,代码如下:

public class SingletonOne {
    private static SingletonOne singletonOne;

    private SingletonOne() {
    }

    public static SingletonOne getInstance() {
        if (singletonOne == null) {
            singletonOne = new SingletonOne();
        }
        return singletonOne;
    }
}

代码中使用静态变量,也称之为类变量保存SingletonOne的实例。无参构造方法私有化,并且不提供其他构造方法。getInstance() 对外提供获取实例的方法。方法内部也符合我们的需求,已经实例化,直接返回实例,如果还是null,去创建这个实例。这种方式称之为懒汉式,是因为类的实例化延迟到第一次getInstance的时候。

看起来上面的代码实现了我们提到的三点需求,无懈可击。没错,一般的场景采用上面的代码足以应付。但是在并发的时候,上面的代码是有问题的。并发时,两个线程对于 singletonOne == null 的判断可能都满足,那么接下来每个线程各自都创建了一个实例。这和单例模式的目标是相违背的。我们需要改造一下。

1.1 线程安全的懒汉单例模式

想要线程安全还不好说,加上 Synchronized 关键字就可以了。修改后代码如下:

public class SingletonTwo {
    private static SingletonTwo singletonTwo;

    private SingletonTwo() {
    }

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

实例化之前为了确保线程安全,我们加上了 synchronized 关键字。你肯定注意到 synchronized 代码块中,又判断了一次 singletonTwo 是否为 null。这是因为你在等待锁的这段时间,可能其他线程已经完成了实例化。所以此处加上 null 的判断,才能确保全局唯一!

看到这里你一定赞叹,这是多么严谨的程序,一定不会有错了!但是事实却不是这样。

如果你学习过多线程,一定对重排序有印象。CPU 为了提高运行效率,可能会对编译后代码的指令做优化,这些优化不能保证代码执行完全符合编写的顺序。但是一定能保证代码执行的结果和按照编写顺序执行的结果是一致的。重排序在单线程下没有任何问题,不过多线程就会出问题了。其实解决方法也很简单,只需要为

singletonTwo 声明时加上 volatile 关键字即可。volatile 修饰的变量是会保证读操作一定能读到写完的值。这种单例也叫做双重检查模式。

代码如下:

public class SingletonTwo {
    private volatile static SingletonTwo singletonTwo;

    private SingletonTwo() {
    }

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

1.2 饿汉式单例模式

有懒汉就有饿汉。饿汉式单例模式在类初实话的时候就会进行实例化。好处是不会有线程安全的问题。问题就是不管程序用不用,实例都早以创建好,这对内存是种浪费。
代码如下:

public class SingletonThree {
    private static SingletonThree singletonOne = new SingletonThree();

    private SingletonThree() {
    }

    public static SingletonThree getInstance() {
        return singletonOne;
    }
}

1.3 内部静态类方式

这次我们先看代码:

public class SingletonFour {

    private SingletonFour() {
    }

    public static SingletonFour getInstance() {
        return SingletonHolder.singletonFour;
    }

    private static class SingletonHolder{
        private static final SingletonFour singletonFour = new SingletonFour();

    }
}

代码中增加了内部静态类 SingletonHolder,内部有一个SingletonFour的实例,并且也是类级别的。那这种方式是饿汉式还是懒汉式?看起来像是饿汉式,因为实例化也是在类初实话的时候进行的。但如果是饿汉式,为什么还要兜这个圈?

其实这是懒汉式。因为内部静态类是现在第一次使用的时候才会去初始化。所以SingletonHolder最初并未被初始化。当第一次执行 return SingletonHolder.singletonFour 时,才会去初始化SingletonHolder类,从而实例化SingletonFour。这种方式利用类加载的机制达到了双重检查模式的效果,而代码更为简洁。

2. 单例模式适用场景

  1. 必须保证全局一个实例。如计数器,多个实例计数就不准确了。再比如线程池,多个实例的话,管理就乱套了。
  2. 一个实例就能满足程序不同地方的使用,并且是线程安全的。比如我们使用 Spring 开发的 bean,绝大多数都可以用单例模式。例如某个 service 类,因为自己不维护状态,线程安全,其实全局只需要一个实例。
  3. 对象被频繁创建和销毁,可以考虑使用单例。
  4. 对象创建比较消耗资源。

3. 小结

我们本节学习了四种单例的实现方式:

  1. 饿汉式非线程安全;
  2. 懒汉式线程安全(双重检查模式);
  3. 饿汉式单例模式;
  4. 内部静态类方式。

单例模式虽然简单,但是想写的严谨,还是需要考虑周全。实际使用中,推荐使用双重检查模式和内部静态类方式。如果实例在你的程序初始化阶段就会被使用,也可以使用饿汉式。非线程安全的懒汉式只能用于非并发的场景,局限性比较大,并不推荐使用。