GoF23:Singleton-单例

1、单实例

1.1、适用场景

有些对象只能存在单个实例,

否则会导致程序异常、资源浪费或不一致等问题。

示例

  1. 线程池(threadpool)
  2. 缓存(cache)
  3. 对话框、处理偏好设置、注册表(registry)
  4. 驱动程序

有些对象适合使用单个实例

比如

  1. 频繁创建和销毁的对象
  2. 重量级对象:创建对象耗时或耗费资源大
  3. 工具类对象
  4. 频繁访问数据库或文件的对象:如数据源、session 工厂。

1.2、如何创建单个实例

两种常用方案

  1. 静态变量(类似单例饿汉式):JVM 加载时就创建实例,可能导致资源的浪费;

    public class MyClass {
    	// 静态变量
        public static final MyClass MY_CLASS = new MyClass();
        
    	// 其它代码
    }
    
  2. 单例模式

    • 可在需要时才创建对象(懒汉式)。
    • 可根据对资源和性能的要求,作相应改进。

2、单例模式

确保一个类只有唯一实例,并提供一个全局访问点

实现方式

  1. 私有构造器:只有类内部的代码可以使用构造器创建实例,避免其它类自行创建实例;

  2. 私有静态变量:即唯一实例。

  3. 公共静态的获取实例方法:即单实例的全局访问点。

    image-20220120221528768

Hint:在 Java 1.2 前,GC 存在一个 Bug。

当单例在全局没有被引用时会被当做垃圾清除,之后会创建一个新的“单例”。

也就是说,实例会发生变化,并非真正意义上的单例。

3、实现方式

3.1、饿汉式

饿汉式加载(aka 急切实例化 Eager Initialization)

适用场景:需频繁创建并使用实例,或创建和运行时耗费资源不大。

  • 实现方式静态变量
  • 优点类加载时完成实例化,线程安全。
  • 缺点:非 Lazy Initialization,若实例从未被使用,则会导致内存浪费。

示例:创建一个静态变量作为单实例。

  1. 声明时赋值

    image-20220121003841988

  2. 静态代码块赋值

    image-20220121003859558

3.2、懒汉式

懒汉式加载(aka 延迟实例化 Lazy Initialization)

方法被调用时实例化,避免内存浪费。

线程不安全版

线程不安全

image-20220121004250428

多线程下的 getInstance() 分析

  1. 假设线程 t1 进入 if 语句,但未执行 new 实例化。
  2. 此时线程 t2 执行 if 判断条件,发现未初始化,也进入 if 语句。
  3. 结果:两个线程分别创建一个实例,导致了多实例。

线程安全版

使用 synchronized 加锁

分析

  1. 每个线程每次调用方法时都会加锁,阻塞其它线程。
  2. 实际上,只需要再方法首次调用时加锁,完成实例初始化后则不再需要加锁。
  3. 因此,本方案会影响性能。

示例

  1. 同步方法:方法签名添加 synchronized

    image-20220121004721565

  2. 同步代码块:实例化操作添加 synchronized

    image-20220121004828666

3.3、双重检查加锁 (🔥)

双重检查加锁(double-checked locking, DCL)

Java 1.5+ 可使用,

懒加载,线程安全,效率高

双重检查机制

  1. 第一次检查实例是否已创建,未创建才进行同步。

  2. 进入同步区块后第二次检查,再次确认实例未被创建,此时才创建实例。

    image-20220121011922051

分析:指令重排序

  1. 对象实例化的三步骤
    1. 分配内存空间
    2. 初始化对象
    3. 将内存空间的地址赋值给变量引用
  2. 如果发生了 JVM 指令重排序:
    1. 可能导致上述步骤 2 和 3 颠倒。
    2. 此时对象尚未初始化,但已经赋值给 instance 变量。
    3. 结果:变量没有正确初始化,可能在运行在发生错误。
  3. 对策:使用 volatile 修饰实例变量,保证可见性和有序性。

3.4、静态内部类 (🔥)

利用类加载机制的特性,

实现懒加载,线程安全,效率高

  1. 声明一个静态内部类,用于实例化静态变量。

    1. 调用外部类的结构时,不会触发内部类的加载。
    2. 显式调用内部类结构时,才会触发内部类的加载。
  2. getInstance() 返回静态内部类的静态变量。

    image-20220121012601921

3.5、枚举 (🔥)

利用 Java 枚举类型,

实现懒加载,线程安全,效率高,防止反序列化。

image-20220121012834968

posted @ 2021-12-17 21:33  Jaywee  阅读(38)  评论(0编辑  收藏  举报

👇