「全网最细 + 实战源码案例」设计模式——单例设计模式
核心思想:https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/9cfc5a70c108406a91775914eab66b37~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgU2xhY2tDbGltYg==:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiODY5MDc1Mjk1NDEwMDA5In0%3D&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1737540386&x-orig-sign=BkeiIWxw2Fp%2FSCuVm7597uw%2Bah8%3D
[*]属于创建型设计模式,核心目的是确保一个类在整个程序运行期间只有一个实例,并提供一个全局访问点来获取该实例。
[*]控制共享资源的访问(如数据库链接、配置管理、日志处理器等)
[*]真实世界类比:政府是单例模式的一个很好的示例。 一个国家只有一个官方政府。 不管组成政府的每个人的身份是什么,“某政府” 这一称谓总是鉴别那些掌权者的全局访问节点。
<hr>结构
https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/7096960f15f545b397821137a674e51e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgU2xhY2tDbGltYg==:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiODY5MDc1Mjk1NDEwMDA5In0%3D&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1737540386&x-orig-sign=tjpANO0FwlotgqG2fe6WryzpdLQ%3D
所有单例的实现都包含以下两个相同的步骤:
[*]将默认构造函数设为私有,防止其他对象使用单例类的 new 运算符。
[*]新建一个静态构建方法作为构造函数。该函数会“偷偷”调用私有构造函数来创建对象,并将其保存在一个静态成员变量中。此后所有对于该函数的调用都将返回这一缓存对象。
如果你的代码能够访问单例类,那它就能调用单例类的静态方法。无论何时调用该方法,它总是会返回相同的对象。
<hr>使用场景:
https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/0d4d1401c2794c6f9d7ab492d62bdb85~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgU2xhY2tDbGltYg==:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiODY5MDc1Mjk1NDEwMDA5In0%3D&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1737540386&x-orig-sign=kPilxu9xrF4NzX2MONkHEM2shTY%3D
1. 需要唯一实例的场景:
[*]配置管理类
[*]日志记录器
[*]数据库连接池
[*]多线程环境中的任务调度器
2. 需要全局共享实例
[*]以避免多个实例引发资源冲突或影响程序逻辑。
<hr>⭐实现方式:
1. 饿汉式(线程安全,类加载时初始化)
1.1. 静态变量式(常见方式)
// 饿汉式(静态变量)public class Singleton { // 1. 私有化构造方法 private Singleton() {} // 2. 创建一个静态变量,保存实例 private static final Singleton instance = new Singleton(); // 3. 提供一个公共的静态方法获取实例 public static Singleton getInstance() { return instance; }}特点:
[*]线程安全:类加载时实例化,JVM 保证线程安全。
[*]缺点:类加载时即创建实例,即使未使用也会占用内存。
<hr>1.2. 静态代码块式
// 饿汉式(静态代码块)public class Singleton { // 1. 私有化构造方法 private Singleton(){} // 2. 创建一个静态对象 private static Singleton instance; // 3. 在静态代码块中创建对象 static { instance = new Singleton(); } // 4. 提供获取对象的方法 public static Singleton getInstance(){ return instance; }}特点:
[*]和静态变量方式类似,在类加载时实例化。
[*]可以在静态代码块中加入额外逻辑,例如异常处理或配置初始化。
<hr>2. 懒汉式(线程不安全,延迟加载)
// 懒汉式,线程不安全public class Singleton { // 1. 私有化构造方法 private Singleton() {} // 2. 定义一个静态变量,用于存储唯一实例 private static Singleton instance; // 3. 定义一个静态方法,用于获取唯一实例 public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; }}优点:
[*]实例在第一次使用时才初始化,节约资源。
缺点:
[*]多线程情况下可能创建多个实例,线程不安全。
<hr>3. 线程安全的懒汉式
3.1. 同步方法
// 懒汉式,同步式,线程安全public class Singleton { // 1. 私有化构造方法 private Singleton() {} // 2. 定义一个静态变量,用于存储 private static Singleton instance; // 3. 定义一个静态方法,用于获取唯一实例 public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; }}缺点:
[*]同步方法会导致性能下降,尤其是高并发访问时。
3.2. 双重检查锁(推荐)
// 懒汉式,双重检查锁方式public class Singleton { // 1. 私有化构造方法 private Singleton() {} // 2. 定义一个静态变量,用于存储实例,volatile保证可见性与有序性,避免指令重排 private static volatile Singleton instance; // 3. 定义一个静态方法,用于获取唯一实例 public static Singleton getInstance() { // 1.第一次判断,如果instance的值为null,则进入同步代码块 if (instance == null) { // 2.同步代码块,保证线程安全 synchronized (Singleton.class) { // 3.第二次判断,如果instance的值为null,则创建实例 if (instance == null) { instance = new Singleton(); } } } return instance; }}优点:
[*]高效,只有在首次实例化时会加锁,之后不会。
注意:volatile 关键字防止指令重排,确保线程安全。
⭐为什么必须要加 volatile?
[*]防止指令重排
[*]
[*]在 Java 中,对象的实例化过程分为三步:
[*]
[*]
[*]分配内存空间
[*]初始化对象
[*]将内存地址赋值给变量
[*]
[*]由于指令重排的存在,步骤 2 和步骤 3 可能被调换执行。例如:
[*]
[*]
[*]线程 A 在执行 instance = new Singleton() 时,可能执行了分配内存和赋值操作,但还未完成初始化。
[*]此时,instance 已经不为 null,但它指向的对象尚未完全初始化。
[*]
[*]如果线程 B 此时调用 getInstance(),判断 instance != null 为真,但实际访问的是一个未初始化完全的对象,这将导致程序出错。
[*]加上 volatile 后,禁止指令重排序,确保初始化顺序正确。
[*]保证变量的可见性
[*]
[*]Java 的内存模型中,每个线程有自己的工作内存。一个线程对变量的修改,可能不会立即被其他线程所见。
[*]加上 volatile 后,保证每次对 instance 的读操作都能获取到最新的值。
[*]
[*]
[*]当线程 A 完成 instance 初始化后,其他线程(如 B 线程)立刻可见,而不会读取到旧值或中间状态。
<hr>3.3. ⭐静态内部类(推荐)
// 懒汉式,静态内部类方式public class Singleton { // 1.构造函数私有化,外部不能new private Singleton() {} // 2.创建静态内部类 private static class SingletonHolder { // 3.创建静态变量,保存实例 private static final Singleton INSTANCE = new Singleton(); } // 3.定义一个静态方法,用于获取唯一实例 public static Singleton getInstance() { return SingletonHolder.INSTANCE; }}原理:
[*]由于 JVM 加载外部类的过程中,不会加载静态内部类,只有内部类的属性/方法被调用时才会被加载,并初始化其静态属性。静态属性由于被 static 修饰,保证只会被实例化一次,并且严格保证实例化顺序。
优点:
[*]线程安全
[*]实现了延迟加载,按需初始化
<hr>4. ⭐枚举单例(最安全,推荐)
// 枚举单例public enum Singleton { INSTANCE;}优点:
[*]简单
[*]天然防止反射和序列化破坏单例
<hr>破坏单例
1. 序列化破坏单例
问题:
序列化和反序列化可以通过 ObjectInputStream 创建一个新的实例,而不是返回现有的单例实例。
示例代码:
import java.io.*;public class Singleton implements Serializable { private static final long serialVersionUID = 1L; private static final Singleton INSTANCE = new Singleton(); private Singleton() {} public static Singleton getInstance() { return INSTANCE; } public static void main(String[] args) throws Exception { Singleton instance1 = Singleton.getInstance(); // 将对象序列化到文件 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton.obj")); oos.writeObject(instance1); oos.close(); // 从文件反序列化对象 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton.obj")); Singleton instance2 = (Singleton) ois.readObject(); // 验证是否为同一个实例 System.out.println(instance1 == instance2); // 输出:false }}原因:
[*]序列化机制会通过反序列化的过程创建一个新的对象实例,而不会调用单例类中的 getInstance() 方法。
解决方案:
实现 readResolve() 方法,确保反序列化时返回现有实例。
private Object readResolve() { return INSTANCE;}<hr>2. 反射破坏单例
问题:
通过反射,能够直接调用私有构造方法,创建多个实例。
示例代码:
import java.lang.reflect.Constructor;public class Singleton { private static final Singleton INSTANCE = new Singleton(); private Singleton() {} public static Singleton getInstance() { return INSTANCE; } public static void main(String[] args) throws Exception { Singleton instance1 = Singleton.getInstance(); // 使用反射创建新实例 Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor(); constructor.setAccessible(true); Singleton instance2 = constructor.newInstance(); // 验证是否为同一个实例 System.out.println(instance1 == instance2); // 输出:false }}原因:
[*]反射可以访问私有构造方法并直接调用,从而绕过单例模式的限制。
解决方案:
[*]在构造方法中防止重复实例化:
private static boolean isCreated = false;private Singleton() { if (isCreated) { throw new RuntimeException("Singleton instance already created!"); } isCreated = true;}
[*]使用枚举单例:
枚举类的单例天然防止反射和序列化破坏。
public enum Singleton { INSTANCE;}<hr>3. 总结
[*]序列化破坏:通过 readResolve() 方法解决。
[*]反射破坏:通过构造方法检查或使用枚举单例解决。
[*]推荐方式:使用 枚举单例,最简单且最安全,能有效防止这两种破坏。
<hr>在源码中的应用
1. ****Runtime 类
[*]简介:Runtime 类允许应用程序与运行时环境交互,比如调用垃圾回收、运行外部命令等。
[*]实现方式:通过 饿汉式单例 实现。
源码分析:
public class Runtime { private static final Runtime currentRuntime = new Runtime(); // 饿汉式实例化 private Runtime() {} // 私有化构造方法 public static Runtime getRuntime() { return currentRuntime; // 返回唯一实例 } public void gc() { // 调用垃圾回收 } public void exit(int status) { // 退出 JVM }}特点:
[*]全局唯一实例。
[*]使用饿汉式,保证线程安全。
<hr>2. ****Desktop 类
[*]简介:Desktop 类用来打开用户默认的应用程序(如浏览器、邮件客户端等)。
[*]实现方式:通过 懒汉式单例 实现。
源码分析:
public final class Desktop { private static Desktop desktop; private Desktop() {} public static synchronized Desktop getDesktop() { if (desktop == null) { desktop = new Desktop(); // 懒汉式单例 } return desktop; } public void browse(URI uri) { // 打开 URI }}特点:
[*]使用同步方法保证线程安全。
[*]懒加载,实例在需要时创建。
<hr>3. ****Logger 类( java.util.logging.Logger )
[*]简介:Logger 是 Java 的日志工具类,用于记录和管理应用程序日志。
[*]实现方式:内部使用单例模式管理全局日志管理器(LogManager)。
源码分析(核心部分):
public class Logger { private static final LogManager manager = LogManager.getLogManager(); // 单例的 LogManager protected Logger(String name, String resourceBundleName) { // Logger 构造方法 } public static Logger getLogger(String name) { return manager.getLogger(name); // 通过单例 LogManager 获取 Logger }}特点:
[*]LogManager 作为单例管理所有 Logger 实例。
[*]getLogger 方法确保每个名称对应的 Logger 是唯一的。
<hr>4. 总结
在 JDK 源码中,单例模式被广泛应用于需要 全局唯一实例 或 资源共享 的场景:
[*]饿汉式:Runtime 类。
[*]懒汉式:Desktop 类。
[*]组合模式:Logger 类中的 LogManager 单例。
这些设计的核心目标是:确保全局状态的一致性、节省资源以及简化管理。
单例模式优缺点:
https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/a11771a8900745faabe8d5c083fa46fc~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgU2xhY2tDbGltYg==:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiODY5MDc1Mjk1NDEwMDA5In0%3D&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1737540386&x-orig-sign=drYtfKwkyvFT228PyU8eFbP%2BfEw%3D
<hr>与其他模式的关系:
[*]外观模式类通常可以转换为单例模式类, 因为在大部分情况下一个外观对象就足够了。
[*]如果你能将对象的所有共享状态简化为一个享元对象, 那么享元模式就和单例类似了。 但这两个模式有两个根本性的不同。
[*]
[*]只会有一个单例实体, 但是享元类可以有多个实体, 各实体的内在状态也可以不同。
[*]单例对象可以是可变的。 享元对象是不可变的。
[*]抽象工厂模式、 生成器模式和原型模式都可以用单例来实现。
页:
[1]