单例模式

本文题图:Photo by Angelo Caputo on Unsplash

本文是设计模式系列中的第二篇,上一篇是 责任链模式

在软件开发的过程中,总是会期待某一个对象在整个应用程序的生命周期中,仅存在一次,该怎么做,我们都知道,就是引入单例模式这个设计模式,让这个对象保持始终只会创建一次。单例模式也是有多种形式,来了解一下吧。

Tasker tasker = new Tasker("Test");
tasker.doTask();

public class Tasker {
    private String name;

    public Tasker(String name) {
        this.name = name;
    }

    public void doTask() {
        System.out.println(name);
    }
}

这就是一个很简单的,创建 Tasker 对象,然后调用一次 doTask 方法。这里,我们不能通过代码的方法对它进行限制全局只创建一次。那怎么样才可以呢?

首先,我们需要将构造方法的访问权限设置为 private,设置为 private 之后,由于外部不能访问构造方法也就不能进行多次调用构造方法获取对象了。

那设置了 private 之后,外面怎么获取 Tasker 对象呢?答案是通过在 Tasker 内部新建一个静态方法 getInstance,然后通过静态方法返回一个 Tasker 对象,由于 Tasker 的构造方法的访问权限是 private,所以在 Tasker 内部访问这个构造方法是没有问题的。

那么我们只需要在 getInstance 方法内部进行检查需要返回的对象是否为空,如果为空则通过调用构造方法进行声明一个 Tasker 对象,如果不为空直接返回。由于 getInstance 方法是静态方法,所以,我们需要在 Tasker 里还需要维护一个静态的 Taster 对象。

Tasker tasker = Tasker.getInstance();
tasker.doTask();

public class Tasker {
    private String name;
    private static Tasker sTasker;

    public static Tasker getInstance() {
        if (sTasker == null) {
            sTasker = new Tasker("Test");
        }
        return sTasker;
    }

    private Tasker(String name) {
        this.name = name;
    }

    public void doTask() {
        System.out.println(name);
    }
}

这样我们就能得到一个简单的单例对象。并且这种形式被称之为懒汉单例模式

这样就行了吗?并不是,这些代码运行在多线程中,可能就会存在一种情况,当一个线程调用 getInstance 判断 sTasker 为空,正在进行调用 Tasker 的构造方法,又有一个线程进入,也判断 sTasker 为空,这样就再次会调用 Tasker 的构造方法,这就不能保证 Tasker 这个类在系统中的唯一性。那要怎么解决这个问题呢?

我们知道,为保证线程安全,Java 有一个关键字 synchronized,我们在调用构造方法的时候加上这个 synchronized 关键字,同时在 sTasker 前加上 volatile 关键字,就能保证调用构造方法的线程安全,就像下面这样。

private static volatile Tasker sTasker;

public static Tasker getInstance() {
    if (sTasker == null) {
        synchronized (Tasker.class) {
            sTasker = new Tasker("Test");
        }
    }
    return sTasker;
}

但是这个是真的能保证是线程安全吗?不见得,当两个线程(A、B)同时访问 getInstance 这个方法时,由于线程 A 获取到了 synchronized 的锁定状态并创建了 Tasker 对象,而当线程 B 获取到 synchronized 的访问状态时,由于 Tasker 已经在线程 A 进行了创建,此时的 sTasker 并不为空,不需要再次调用 构造方法进行获取一个新的实例,所以,应该在 synchronized 锁定的代码块中再加入一个判断 sTasker 是否为空。这种形式的单例模式,我们称之为双重检查锁定实现的懒汉式单例模式

private static volatile Tasker sTasker;

public static Tasker getInstance() {
		if (sTasker == null) {
				synchronized (Tasker.class) {
						if (sTasker == null) {
								sTasker = new Tasker("Test");
						}
				}
		}
		return sTasker;
}

那我能不能在声明这个变量(sTasker)的时候,就直接调用其构造方法来进行初始化操作。这样不就既保证了当前类实例在系统中的唯一性也保证了线程安全吗?没错,这样也是单例模式的另一种实现方式,叫饿汉式单例模式

private static Tasker sTasker = new Tasker("Test");

public static Tasker getInstance() {
    return sTasker;
}

不过,饿汉式单例在当前类加载到内存中的时候就进行了初始化操作,假设我们没有使用到 sTasker,那去创建这个 sTasker 对象就相当于是浪费了,浪费可耻。而且当我们在构造方法中初始化过多的资源信息时也会影响整个系统的加载时长。从资源的使用角度来看,饿汉式的单例模式是不如双重检查的懒汉式的单例模式的,但双重检查的懒汉式单例也有属于他自己的问题,假设我们在构造方法中初始化了较多的资源,由于引入了 synchronized,所以在 getInstance 时将导致系统性能受到一定的影响。

那就没有两全其美的方法吗?有的,这种方法被称之为 Initializtion Demand Holder (IoDH)。

在 Java 中,通过在实例类中增加一个静态的内部类,在内部类中创建单例对象,再通过单例类的 getInstance 返回实例供外部使用。

private static class Inner {
    private static final Tasker TASKER = new Tasker("Test");
}

public static Tasker getInstance() {
    return Inner.TASKER;
}

由于类加载时不会实例化 Tasker 对象,所以只在 调用 getInstance 方法时才会调用。而在调用 getInstance 时才进行加载 Inner 类并创建一个 Tasker 实例,由 JVM 保证了其线程安全。