创建型模式之单例模式

编程

1 概述

单例模式应该是最简单,同时又是最复杂的一种创建型模式。因为大家都知道这个模式:无非就是保证某个对象在系统中只存在一个实例。然而想要真正实现一个完美的单例模式,却不简单。

2 单例模式

一般单例模式的实现,都需要包含两个步骤:

  1. 将类的构造函数私有化。
  2. 提供一个public的方法,以供外界获取唯一的实例。

下面将一一介绍单例模式的各种实现方式。

3 案例

3.1 注册表式

提供一个注册表类,来维护所有单例的实例

public class Test {

public static void main(String[] args) {

SampleClass singleton1 = Registry.getInstance(SampleClass.class);

SampleClass singleton2 = Registry.getInstance(SampleClass.class);

System.out.println("Registry singleton instance1: " + singleton1.hashCode());

System.out.println("Registry singleton instance2: " + singleton2.hashCode());

System.out.println("We can broke singleton by new a instance through class"s construct method");

SampleClass singleton3 = new SampleClass();

System.out.println("Registry singleton instance3: " + singleton3.hashCode());

}

}

public class Registry {

private static Map<Class, Object> registry = new ConcurrentHashMap<>();

private Registry() {};

public static synchronized <T> T getInstance(Class<T> type) {

Object obj = registry.get(type);

if (obj == null) {

try {

obj = type.newInstance();

} catch (IllegalAccessException | InstantiationException e) {

e.printStackTrace();

}

registry.put(type, obj);

}

return (T) obj;

}

}

public class SampleClass {

}

输出:

Registry singleton instance1: 21685669

Registry singleton instance2: 21685669

We can broke singleton by new a instance through class"s construct method

Registry singleton instance3: 2133927002

用注册表实现的单例其实是伪单例,因为它只能保证从注册表中获取的对象是全局唯一的。如果我们不从注册表获取,而是直接new一个实例,这显然破坏了单例模式。我们熟悉的Spring框架,就是用这种模式实现的单例,其中的Registry就是BeanFactory

要从根本上实现实例的全局唯一,我们必须在单例类本身下功夫。

3.1 饿汉式----静态属性

将实例作为类的一个静态变量,来实现唯一性:

public class StaticFieldTest {

public static void main(String[] args) {

StaticFieldSingleton fieldSingleton1 = StaticFieldSingleton.getInstance();

StaticFieldSingleton fieldSingleton2 = StaticFieldSingleton.getInstance();

System.out.println("StaticFieldSingleton instance1: " + fieldSingleton1.hashCode());

System.out.println("StaticFieldSingleton instance1: " + fieldSingleton2.hashCode());

}

}

public class StaticFieldSingleton {

private static StaticFieldSingleton singletonInstance = new StaticFieldSingleton();

// 将构造方法私有化

private StaticFieldSingleton(){};

// 提供唯一的接口,供外部获取唯一的变量

public static StaticFieldSingleton getInstance() {

return singletonInstance;

}

}

输出:

StaticFieldSingleton in multi-thread instance: 837048303

StaticFieldSingleton in multi-thread instance: 837048303

当类StaticFieldSingleton被加载进JVM的时候,类的实例会作为类的静态属性,随着类一起初始化。这种实现方式其实是依靠类加载器来保证实例的唯一性。优点是,不需要考虑多线程加锁,实现起来比较简单。缺点是,无论后续是否会用到,实例都会在class被加载的时候被创建好。这对于内存资源比较宝贵的场景,或者目标是某些如File System的大对象的时候,会导致资源的浪费。同时,这种方式也无法提供对异常的处理,在某些情况下,会导致程序出错。

3.2 饿汉式----静态块

将类实例的初始化放在类的静态块中:

public class StaticBlockTest {

public static void main(String[] args) {

for (int i = 0; i < 2; i++) {

new Thread(() -> {

StaticBlockSingleton staticBlockSingleton = StaticBlockSingleton.getInstance();

System.out.println("StaticBlockSingleton in multi-thread instance: " + staticBlockSingleton.hashCode());

}).start();

}

}

}

public class StaticBlockSingleton {

private static StaticBlockSingleton singletonInstance;

private StaticBlockSingleton(){};

// 静态块会在类被加载进内存的时候被执行

static {

try {

singletonInstance = new StaticBlockSingleton();

} catch (Exception e) {

e.printStackTrace();

}

}

public static StaticBlockSingleton getInstance() {

return singletonInstance;

}

}

输出:

StaticBlockSingleton in multi-thread instance: 2132107705

StaticBlockSingleton in multi-thread instance: 2132107705

静态块中初始化与静态变量上初始化本质上是一样的,都是通过类加载器来保证实例只会被初始化一次。区别是,静态块初始化可以做异常的捕获与处理,同时还允许我们在静态块中做一些额外的事情,比静态变量的方式更自由。

但两种饿汉式都不可避免地会造成额外内存的占用,于是出现了按需加载的懒汉式创建方式。

3.3 懒汉式----基础版

将类实例的初始化放在方法中。只有当方法第一次被访问的时候,去初始化实例:

public class SynchronizedTest {

public static void main(String[] args) {

for (int i = 0; i < 2; i++) {

new Thread(() -> {

SynchronizedSingleton synchronizedSingleton = SynchronizedSingleton.getInstance();

System.out.println("SynchronizedSingleton in multi-thread instance: " + synchronizedSingleton.hashCode());

}).start();

}

}

}

public class SynchronizedSingleton {

private static SynchronizedSingleton singletonInstance;

private SynchronizedSingleton(){};

// 加了同步锁,保证new SynchronizedSingleton()只会被第一个线程访问

public static synchronized SynchronizedSingleton getInstance() {

if (singletonInstance == null) {

singletonInstance = new SynchronizedSingleton();

}

return singletonInstance;

}

}

输出:

SynchronizedSingleton in multi-thread instance: 554449003

SynchronizedSingleton in multi-thread instance: 554449003

懒汉式解决了饿汉式存在的最大问题:可能导致的内存浪费。只有当getInstance()方法第一次被访问的时候,实例才会去真正创建。而方法上加了synchronized,保证了后续对方法的访问,都只会返回之前创建好的实例,保证了唯一性。

这种方式的不足是,每次对getInstance()方法的访问,都需要获取锁,众所周知,锁的获取与释放是一笔昂贵的开销。而事实上只有当第一次实例创建的时候需要加锁。于是有了改进的方式:双检锁。

3.4 懒汉式----双检锁

双检锁(Double Check Lock)是一个很多人都熟悉的概念,是上述模式的增强版。实现如下:

public class DoubleCheckLockTest {

public static void main(String[] args) {

for (int i = 0; i < 2; i++) {

new Thread(() -> {

DoubleCheckLockSingleton doubleCheckLockSingleton = DoubleCheckLockSingleton.getInstance();

System.out.println("DoubleCheckLockSingleton in multi-thread instance: " + doubleCheckLockSingleton.hashCode());

}).start();

}

}

}

public class DoubleCheckLockSingleton {

// 变量必须声明为volatile,否则可能会得到一个“半初始化”的实例

private static volatile DoubleCheckLockSingleton singletonInstance;

private DoubleCheckLockSingleton(){};

// 若实例已经被创建,则不需要再进入同步块

// 若实例还没创建,则在同步块中检查并创建实例

public static DoubleCheckLockSingleton getInstance() {

DoubleCheckLockSingleton instance = singletonInstance;

if (instance == null) {

synchronized (DoubleCheckLockSingleton.class) {

instance = singletonInstance;

if (instance == null) {

instance = singletonInstance = new DoubleCheckLockSingleton();

}

}

}

return instance;

}

}

输出:

DoubleCheckLockSingleton in multi-thread instance: 837048303

DoubleCheckLockSingleton in multi-thread instance: 837048303

单例模式中,水最深的应该就是是双检锁了。在上述实现中,有几个要点:

  1. 为什么需要两次if检查:第一次if检查在synchronized块之外,当实例已经被创建好之后,可以立即返回。第二次if检查,是因为在高并发的情况下,可能会有好多线程走到第一个if块中,去争抢synchronized锁,我们必须保证只有第一个抢到锁的线程能创建实例,所以后面的线程必须再进行一次if判断,发现实例已经被第一个抢到锁的线程初始化好了,直接返回该实例。这也是双检名字的由来。
  2. 为什么成员变量singletonInstance要声明为volatile:因为new DoubleCheckLockSingleton()其实并不是一个原子操作,主要可以分为给实例分配堆内存,执行类的构造函数,将实例引用赋给调用者三步。而由于重排序的存在,在某一些机器上,第三步会先于第二步发生,于是可能出现,线程A走到了new DoubleCheckLockSingleton(),但并未执行完构造函数时,线程B发现instance != null了,于是对instance的属性进行访问,结果看到的属性都是默认值。而JMMJava1.5之后进行了增强,volatile关键字可以禁止编译器的重排序,并会在volatile关键字修饰的变量前后适当位置添加内存屏障,保证程序不会读到半初始化的实例。关于JMM的增强,可以扩展阅读Doug Lea大神的文章。
  3. 为什么要加局部变量instance:加这个局部变量,主要是为了提高程序的性能。因为成员变量singletonInstance是声明为volatile的,而所有对volatile变量的操作(读写)都必须与主内存交互,开销相对较大。加局部变量可以减少与volatile变量的交互。这也是java.util.concurrent包中很多工具类的常见做法。

到这里,似乎双检锁的方案已经很完美了,确实,这也是被很多人所采用的单例模式实现方案。但其实懒汉式还有一种更为通用的实现方式。

3.5 懒汉式----静态内部类

引入一个静态内部类,来实现对静态变量的延迟加载:

public class InnerClassWrappedTest {

public static void main(String[] args) {

for (int i = 0; i < 2; i++) {

new Thread(() -> {

InnerClassWrappedSingleton innerClassWrappedSingleton = InnerClassWrappedSingleton.getInstance();

System.out.println("InnerClassWrappedSingleton in multi-thread instance: " + innerClassWrappedSingleton.hashCode());

}).start();

}

}

}

public class InnerClassWrappedSingleton {

private InnerClassWrappedSingleton(){};

// 内部类持有单例,仅当getInstance()方法被调用的时候,SingletonHolder类才会被加载

// final关键字保证了不会得到“半初始化”的实例

private static class SingletonHolder {

private static final InnerClassWrappedSingleton instance = new InnerClassWrappedSingleton();

}

public static InnerClassWrappedSingleton getInstance() {

return SingletonHolder.instance;

}

}

输出:

InnerClassWrappedSingleton in multi-thread instance: 2132107705

InnerClassWrappedSingleton in multi-thread instance: 2132107705

上述实现其实可以看作是饿汉式----静态块的升级版,只不过把实例的初始化,放到了静态内部类中。而该静态内部类只有在getInstance()被调用的时候,才会被加载,从而对单例进行初始化。同样,由类加载器保证了,只有一个实例会被创建。同时,final关键字在Java1.5之后也进行了增强,可以保证得到的一定是一个完整的单例。

这种方式是本人觉得比较好的方式,因为实现简单线程安全,而且适用性很强。

3.6 破坏单例----序列化

其实所有上述的实现方式,都不可能完全保证类的唯一,因为尽管我们把类的构造器设为了private,但仍然有办法用其他方式创建新的实例。比如不巧,单例的类正好实现了Serializable接口,那么黑客们可以通过序列化的方式,得到一个新的“单例”:

public class SingletonDestroyerSerialization {

public static void main(String[] args) throws IOException, ClassNotFoundException {

InnerClassWrappedSingleton instance1 = InnerClassWrappedSingleton.getInstance();

ByteArrayOutputStream baos = new ByteArrayOutputStream();

ObjectOutputStream oos = new ObjectOutputStream(baos);

// 将单例序列化

oos.writeObject(instance1);

ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());

ObjectInputStream ois = new ObjectInputStream(bais);

// 反序列化,创建一个新的“单例”

InnerClassWrappedSingleton instance2 = (InnerClassWrappedSingleton) ois.readObject();

System.out.println("singleton instance1: " + instance1.hashCode());

System.out.println("singleton instance2: " + instance2.hashCode());

}

}

public static class InnerClassWrappedSingleton implements Serializable {

private InnerClassWrappedSingleton(){};

private static class SingletonHolder {

private static final InnerClassWrappedSingleton instance = new InnerClassWrappedSingleton();

}

public static InnerClassWrappedSingleton getInstance() {

return SingletonHolder.instance;

}

}

输出:

singleton instance1: 1173230247

singleton instance2: 764977973

显然,输出了不同的hashcodeJVM中存在了两个“单例”对象。

为了防止以上情况出现,我们可以在单例类中,添加一个readResolve()方法,并返回单例实例。这样,在反序列化之后,我们得到的依然是原先的实例:

public static class InnerClassWrappedSingleton implements Serializable {

private InnerClassWrappedSingleton(){};

private static class SingletonHolder {

private static final InnerClassWrappedSingleton instance = new InnerClassWrappedSingleton();

}

public static InnerClassWrappedSingleton getInstance() {

return SingletonHolder.instance;

}

// 添加此方法,防止序列化与反序列化创建新的实例

private Object readResolve() {

return SingletonHolder.instance;

}

}

3.7 破坏单例----反射

如果说序列化与反序列化我们还有应对的办法,那么对于反射攻击,上述所有的实现方案,都无可奈何:

public class SingletonDestroyerRefelct {

public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException {

InnerClassWrappedSingleton instance1 = InnerClassWrappedSingleton.getInstance();

System.out.println("singleton instance1: " + instance1.hashCode());

Constructor[] constructors = InnerClassWrappedSingleton.class.getDeclaredConstructors();

for (Constructor constructor : constructors) {

// 利用反射,创建一个新的“单例”变量

constructor.setAccessible(true);

InnerClassWrappedSingleton instance2 = (InnerClassWrappedSingleton) constructor.newInstance();

System.out.println("singleton instance2: " + instance2.hashCode());

break;

}

}

}

public static class InnerClassWrappedSingleton {

private InnerClassWrappedSingleton(){};

private static class SingletonHolder {

private static final InnerClassWrappedSingleton instance = new InnerClassWrappedSingleton();

}

public static InnerClassWrappedSingleton getInstance() {

return SingletonHolder.instance;

}

}

输出:

singleton instance1: 1735600054

singleton instance2: 21685669

反射其实类似Java中的一个后门,非常强大,它能破坏单例模式也是情理之中。类似JSON序列化与反序列化,也能创建多个不同的“单例”,利用的也是反射机制。

3.8 究极单例----Enum

有没有办法防止反射调用破坏单例呢?答案是肯定的,即用enum创建单例:

public enum EnumSingleton {

INSTANCE;

public void doSomething() {

}

}

首先,JVMenum型变量的序列化与反序列化做了特殊处理,保证反序列化之后得到的依然是内存中的那个enum

第二,Java从语言层面保证,无法通过反射创建enum类型变量。

所以,如果说要选一种最安全的单例模式实现方案,那非Enum模式莫属。这也是「Effective Java」的作者Joshua Bloch所推荐的方式。

4 总结

本文介绍了形形色色很多的单例模式,其实也并不是越到后面的实现越好,而是要看每个版本的特性,选择最适合自己项目的那个版本。

文中例子的github地址

以上是 创建型模式之单例模式 的全部内容, 来源链接: utcz.com/z/514854.html

回到顶部