GoF23:Singleton-单例
1、单实例
1.1、适用场景
有些对象只能存在单个实例,
否则会导致程序异常、资源浪费或不一致等问题。
示例:
- 线程池(threadpool)
- 缓存(cache)
- 对话框、处理偏好设置、注册表(registry)
- 驱动程序
有些对象适合使用单个实例
比如:
- 频繁创建和销毁的对象
- 重量级对象:创建对象耗时或耗费资源大
- 工具类对象
- 频繁访问数据库或文件的对象:如数据源、session 工厂。
1.2、如何创建单个实例
两种常用方案
-
静态变量(类似单例饿汉式):JVM 加载时就创建实例,可能导致资源的浪费;
public class MyClass { // 静态变量 public static final MyClass MY_CLASS = new MyClass(); // 其它代码 }
-
单例模式:
- 可在需要时才创建对象(懒汉式)。
- 可根据对资源和性能的要求,作相应改进。
2、单例模式
确保一个类只有唯一实例,并提供一个全局访问点。
实现方式:
-
私有构造器:只有类内部的代码可以使用构造器创建实例,避免其它类自行创建实例;
-
私有静态变量:即唯一实例。
-
公共静态的获取实例方法:即单实例的全局访问点。
Hint:在 Java 1.2 前,GC 存在一个 Bug。
当单例在全局没有被引用时会被当做垃圾清除,之后会创建一个新的“单例”。
也就是说,实例会发生变化,并非真正意义上的单例。
3、实现方式
3.1、饿汉式
饿汉式加载(aka 急切实例化 Eager Initialization)
适用场景:需频繁创建并使用实例,或创建和运行时耗费资源不大。
- 实现方式:静态变量。
- 优点:类加载时完成实例化,线程安全。
- 缺点:非 Lazy Initialization,若实例从未被使用,则会导致内存浪费。
示例:创建一个静态变量作为单实例。
-
声明时赋值:
-
静态代码块赋值:
3.2、懒汉式
懒汉式加载(aka 延迟实例化 Lazy Initialization)
方法被调用时实例化,避免内存浪费。
线程不安全版
线程不安全
多线程下的 getInstance() 分析
- 假设线程 t1 进入 if 语句,但未执行 new 实例化。
- 此时线程 t2 执行 if 判断条件,发现未初始化,也进入 if 语句。
- 结果:两个线程分别创建一个实例,导致了多实例。
线程安全版
使用 synchronized 加锁
分析:
- 每个线程每次调用方法时都会加锁,阻塞其它线程。
- 实际上,只需要再方法首次调用时加锁,完成实例初始化后则不再需要加锁。
- 因此,本方案会影响性能。
示例
-
同步方法:方法签名添加
synchronized
。 -
同步代码块:实例化操作添加
synchronized
。
3.3、双重检查加锁 (🔥)
双重检查加锁(double-checked locking, DCL)
Java 1.5+ 可使用,
懒加载,线程安全,效率高。
双重检查机制:
-
第一次检查实例是否已创建,未创建才进行同步。
-
进入同步区块后第二次检查,再次确认实例未被创建,此时才创建实例。
分析:指令重排序
- 对象实例化的三步骤:
- 分配内存空间
- 初始化对象
- 将内存空间的地址赋值给变量引用
- 如果发生了 JVM 指令重排序:
- 可能导致上述步骤 2 和 3 颠倒。
- 此时对象尚未初始化,但已经赋值给 instance 变量。
- 结果:变量没有正确初始化,可能在运行在发生错误。
- 对策:使用 volatile 修饰实例变量,保证可见性和有序性。
3.4、静态内部类 (🔥)
利用类加载机制的特性,
实现懒加载,线程安全,效率高。
-
声明一个静态内部类,用于实例化静态变量。
- 调用外部类的结构时,不会触发内部类的加载。
- 显式调用内部类结构时,才会触发内部类的加载。
-
getInstance() 返回静态内部类的静态变量。
3.5、枚举 (🔥)
利用 Java 枚举类型,
实现懒加载,线程安全,效率高,防止反序列化。