设计之禅——单例模式详解

时间:2022-07-24
本文章向大家介绍设计之禅——单例模式详解,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

一、前言

有时候我们只需要一个类只有一个对象,如,线程池、缓存、windows的任务管理器、注册表等,因此就有了单例模式,确保了一个类只存在一个实例。单例模式的实现非常简单,但是其中的细节也需要注意。下面我们就来看看他的各种实现。

二、实现

单例模式的实现方式有很多,根据是否立即创建对象分为“懒汉”和“饿汉”两大类别,即是否在类加载时立即创建对象,如果该对象频繁被使用,可以使用“饿汉式”提高效率;反之则可以使用“懒汉式”来避免内存的浪费。而“懒汉式”的创建在多线程环境下则有许多方式来保证线程安全。

1. 懒汉式-线程不安全

public class Singleton {
	
    public static Singleton instance;
    
 	// 私有化构造方法,保证外部无法创建对象
    private Singleton() {}

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

        return instance;
    }

}

这种只能保证在单线下获取到单例对象,并且在需要的时候才会创建对象,故此称为“懒汉式”。但是因为new Singleton()该操作并不是原子操作,当线程1执行到此时,可能还并未创建实例,那么线程2在判断instance==null时就会为真,从而产生多个实例。

2. 懒汉式-线程安全

public class Singleton {

    private static Singleton instance;

	// 私有化构造方法,保证外部无法创建对象
    private Singleton() {}

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

        return instance;
    }

}

这种方式保证了线程安全,但是效率非常低,因此一般不推荐使用。

3. 饿汉式

public class Singleton {

    private static Singleton instance = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return instance;
    }

}

与懒汉式的区别是类加载的时候立即创建了对象实例,保证了对象始终只会有一个,但是如果该对象一直不被使用,就会浪费内存资源。

4. 静态内部类

public class Singleton {

    private Singleton() {}

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

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

}

使用静态内部类来实现单例其实也是懒汉式的优化实现,利用类初始化时线程安全这一特点来创建单例对象,同时因为是在静态内部类中,有且仅当getInstance()方法被调用时才会被初始化,所以也避免了内存的浪费。

5. 双重校验锁

public class Singleton {

    private static volatile Singleton instance;

    private Singleton() {}

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

        return instance;
    }

}

该方式是“饿汉式”的变种,保留了“饿汉式”的特点的同时保证了线程的安全。但是,需要注意的是volatile关键字是必须的,在网上很多文章上看到都没带这个关键字,如果不加可能会导致程序的崩溃。因此该方法只能在JDK1.5后使用。 volatile是保证线程之间的可见性。倘若没有该关键字,假设线程1和线程2先后调用getInstance()方法,当线程1进入方法时判断instance=null,因此去执行new Singleton()创建实例,上文提到该操作并非原子操作,会被编译为三条指令:

  1. 分配对象的内存空间;
  2. 初始化对象;
  3. 将对象指向内存地址;

而jvm会为了执行效率而进行指令重排,重排后的指令顺序为:1->3->2,当指令执行完第3条指令,此时线程2进入方法进行第一次判断时,就会得到一个并不完整的对象实例(因为对象还未初始化,只是分配了内存空间),接着线程1执行完第2条指令,又会返回这个实例的完全态,但并不会立即刷新主内存,所以线程2并不能访问到,程序就会出现错误导致崩溃。而volatile就是为了处理这个问题,他能保证当某个线程改变对象实例后,立即刷新主内存,让其他线程能够同样获取到相同的实例对象,就不会出现不一致的问题了。

6. 枚举

public enum Singleton {
    INSTANCE;
}

用枚举的方式创建单例非常简单明了,它本身能保证线程的安全,还能防止反序列化(readObject())导致对象不一致的问题,唯一的缺点则是同饿汉式一样会立即创建对象实例(反编译后可以看到),如果不考虑这点枚举应是单例实现的最佳方式,也是《Effective Java》作者推荐的方式。

三、总结

单例模式是比较常用的模式之一,本文总结了6种实现方式,可以感受到看似简单的代码背后涉及到的细节非常多,因此也是非常考验我们的基本功。在本文中并没有考虑反射入侵的情况,有兴趣的读者们可自行研究。