vlambda博客
学习文章列表

初学设计模式----单例模式

单例模式

定义:保证一个类只能被实例化一次,提供一个访问它的全局访问点。

特点:

  • 只有一个实例,需要获取的时候,如果该实例已经创建,则返回该实例,如果没有创建则创建一个实例保存并返回。

  • 核心是创建一个唯一对象。在Javascript中这个及其简单,在全局创建一个对象即可;也可以自定义创建某一实例,保证在其他地方使用时保证唯一性。

  • 属于创建型模式

优点:

  1. 内存中只有一个实例,减少了内存的开销。在频繁使用时,省去了大量时间

  2. 避免了对资源的重复占用

缺点:

  1. 违反了单一职责原则,一个类应该只关注内部逻辑,而不应该关心外部实现

  2. 简答的单例模式设计开发比较简单,但是复杂的单例模式需要考虑线程安全等并发问题,引入了部分复杂度

  3. 由于提供了一种单点访问,所以导致模块之间的耦合性增强,不利于单元测试

使用场景:

  1. 例如vuex中的store等需要唯一的实例

  2. 需要在全局使用的唯一的对象实例,如购物车、登录框、第三库等

  3. 需要缓存的信息可以用单例模式来实现

设计实现:

  1. 考虑的因素:线程安全、延迟加载、代码安全(防止序列化攻击、防止反射攻击等)、性能因素

  2. 实现方式:

    • 饿汉式:在单例对象实例进行声明引用的时候就进行实例化创建对象实例,常用方式

    • 懒汉式(线程安全、线程非安全):单例类对象实例懒加载。不会提前创建对象,只有在使用对象实例的时候才会创建

    • 双重检查(DCL:Double Checked Locking):又称双检锁,即对实例对象进行两次检查

    • 静态内部类

    • 枚举:如果有关于反序列化创建对象会考虑使用枚举实现单例模式

实现方式 线程安全 并发性能好 可延迟加载 序列化/反序列化安全 防反射攻击
饿汉式 Y Y N N N
懒汉式(不加锁) N Y Y N N
懒汉式(加锁) Y N Y N N
DCL Y Y Y N N
静态内部类 Y Y Y N N
枚举 Y Y N Y Y

例子:

// 1.饿汉模式class Singleton { static instance = new Singleton()construtor(){}getInstance(){ return this.instance }}
// 饿汉模式public class Singleton { private static Singleton instance = new Singleton(); private Singleton(){} public static Singleton getInstance(){ return instance; }}

以上都是通过getInstance获取对象,保证了单例对象的唯一。

// 2.懒汉模式class Singleton { constructor() { if (!Singleton.instance) { Singleton.instance = this; } return Singleton.instance; }}
const single = new Singleton()const single2 = new Singleton()console.log(single === single2); // true

// 懒汉式(线程不安全,不可用)public class Singleton { private static Singleton instance = null; private Singleton() { } public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; }}// 懒汉模式中最简单的一种写法,只有在第一次访问时才会实例化,达到了懒加载的效果。但是在多线程时,会出现知名问题。如果多个线程同时访问,就会出现多次实例化的结果,所以改写法不可用
// 懒汉式(线程安全,可用)public class Singleton { private static Singleton instance = null; private Singleton() { } public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; }}// 对getInstance加了锁的处理,保证在同一时刻只能有一个线程访问并获得实例// 但缺点即使使用了synchronized修饰整个方法,每个线程访问都要进行同步,导致效率低下,下面的DCL改进了这种写法

// DCL(双重检查)// 使用同步块加锁的方法// 会有两次检查instance == null// -- 一次在同步块外// -- 一次在同步块内// --因为会有多个线程一起进入同步块外的if中// --如果不在同步块内不进行二次检验就会导致生成多个实例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:

  • 对于计算机中的指令而言 ,CPU和编译器为了提升程序的执行效率,通常会按照一定的规则对指令进行优化

  • 如果两条指令互不依赖,那么指令执行的顺序可能不是源码的编写顺序

  • 形如instance = new Instance(),方法创建实例执行分为三步:

    1. 分配对象内存空间: 给新创建的Instance对象分配内存

    2. 初始化对象: 调用单例类的构造函数来初始化成员变量

      • 分配对象内存空间

      • 初始化对象

    • CPU和编译器在指令重排时,不会关心指令重排执行是否影响多线程的执行结果. 如果不加volatile关键字,如果有多个线程访问getInstance()方法时,如果刚好发生了指令重排,可能会出现以下情况:

      • 如果此时有另一个线程调用getInstance() 方法,在第一个if的判断时结果就为false, 就会直接返回没有初始化完成的instance, 这样可能会导致程序NPE异常

  • 使用volatile的原因是禁止指令重新排序:

    • volatile变量进行赋值操作后会有一个内存隔离

    • 读操作不会重排序到内存隔离之中

    • 比如在上面操作中,读操作必须在执行完1,2,3或者1,3,2步骤之后才会执行读取到结果,否则不会读取到相关结果

// 静态内部类public class Singleton { private Singleton() {} private static class SingletonInstance { private static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return SingletonInstance.INSTANCE; }}

使用静态内部类模式创建单例类实例是使用JVM机制保证线程安全:

  • 静态单例对象没有作为单例类的成员变量直接实例化,所以当类加载时不会实例化单例类

  • 第一次调用getInstance() 方法时将加载静态内部类SingletonInstance. 在静态内部类中定义了一个static类型的变量instance, 这时会首先初始化这个变量

  • 通过JVM来保证线程安全,确保该成员变量只初始化一次

  • 由于getInstance() 方法并没有加线程锁,所以对性能没有什么影响

静态内部类的优点:

  • 静态内部类SingletonInstance是私有的,只能通过getInstance() 方法进行访问,所以这是懒加载的

  • 读取实例时不会进行同步锁的获取,性能较好

  • 静态内部类不依赖JDK版本

// 枚举public enum Singleton { INSTANCE;}

使用枚举方式实现单例的最大特点是非常简单。可以通过Enum.INSTANCE来访问实例,和getInstance() 方法比较更加简单

枚举的创建默认就是线程安全的方法,而且能防止反射以及反序列化导致重新创建新的对象

  • Enum类内部使用Enum类型判定防止通过反射创建新的对象

  • Enum类通过对象的类型和枚举名称将对象进行序列化,然后通过valueOf() 方法匹配枚举名称找到内存中的唯一对象实例,这样可以防止反序列化时创建新的对象

特别说明:

  • 懒汉式和饿汉式实现的单例模式破坏 : 无论是通过懒汉式还是饿汉式实现的单例模式,都可能通过反射和反序列化破坏掉单例的特性,可以创建多个对象

  • 反射破坏单例模式: 利用反射,可以强制访问单例类的私有构造器,创建新的对象

public static void main(String[] args) {// 利用反射获取单例类的构造器Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();// 设置访问私有构造器 constructor.setAccessiable(true); // 利用反射创建新的对象 Singleton newInstance = constructor.newInstance(); // 通过单例模式创建单例对象 Singleton singletonInstance = Singleton.getInstance(); // 此时这两个对象是两个不同的对象,返回false System.out.println(singletonInstance == newInstance);}
  • 反序列化破坏单例模式: 通过readObject() 方法读取对象时会返回一个新的对象实例

public static void main(String[] args) {// 创建一个输出流对象ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("Singleton.file"));// 将单例类对象写入到文件中Singleton singletonInstance = Singleton.getInstance();os.writeObject(singleton);// 从文件中读取单例对象File file = new File("Singleton.file");ObjectInputStream is = new ObjectInputStream(new FileInputStream(file));Singleton newInstance = (Singleton)is.readObject();// 此时这两个对象是两个不同的对象,返回falseSystem.out.println(singletonInstance == newInstance);}

参考资料:

  1. 也来谈谈懒汉和饿汉,详细解析单例模式的六种实现方式!- 掘金 (juejin.cn)

  2. 面试官所认为的单例模式 - 掘金 (juejin.cn)

  3. JavaScript设计模式es6(23种) - 掘金 (juejin.cn)

  4. 在JS中总结一下设计模式(单例-懒汉,单例-饿汉,工厂,代理,观察者)_黯蕶-veteran的博客-CSDN博客