「全网最细 + 实战源码案例」设计模式——单例设计模式

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 单例模式是一种创建型设计模式,确保一个类在整个程序运行期间只有一个实例,并提供一个全局访问点来获取该实例。它常用于控制共享资源的访问,如数据库连接、配置管理等。实现方式包括饿汉式(类加载时初始化)、懒汉式(延迟加载)、双重检查锁、静态内部类和枚举单例等。其中,枚举单例最简单且安全,能有效防止反射和序列化破坏。

核心思想:

  • 属于创建型设计模式,核心目的是确保一个类在整个程序运行期间只有一个实例,并提供一个全局访问点来获取该实例。
  • 控制共享资源的访问(如数据库链接、配置管理、日志处理器等)
  • 真实世界类比:政府是单例模式的一个很好的示例。 一个国家只有一个官方政府。 不管组成政府的每个人的身份是什么,“某政府” 这一称谓总是鉴别那些掌权者的全局访问节点。

结构

所有单例的实现都包含以下两个相同的步骤:

  • 将默认构造函数设为私有,防止其他对象使用单例类的 new 运算符。
  • 新建一个静态构建方法作为构造函数。该函数会“偷偷”调用私有构造函数来创建对象,并将其保存在一个静态成员变量中。此后所有对于该函数的调用都将返回这一缓存对象。

如果你的代码能够访问单例类,那它就能调用单例类的静态方法。无论何时调用该方法,它总是会返回相同的对象。


使用场景:

1. 需要唯一实例的场景:

  • 配置管理类
  • 日志记录器
  • 数据库连接池
  • 多线程环境中的任务调度器

2. 需要全局共享实例

  • 以避免多个实例引发资源冲突或影响程序逻辑。

⭐实现方式:

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 保证线程安全。
  • 缺点:类加载时即创建实例,即使未使用也会占用内存。

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;
    }
}

特点:

  • 和静态变量方式类似,在类加载时实例化。
  • 可以在静态代码块中加入额外逻辑,例如异常处理或配置初始化。

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;
    }
}

优点:

  • 实例在第一次使用时才初始化,节约资源。

缺点:

  • 多线程情况下可能创建多个实例,线程不安全。

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

  1. 防止指令重排
    1. 在 Java 中,对象的实例化过程分为三步:
      1. 分配内存空间
      2. 初始化对象
      3. 将内存地址赋值给变量
    1. 由于指令重排的存在,步骤 2 和步骤 3 可能被调换执行。例如:
      1. 线程 A 在执行 instance = new Singleton() 时,可能执行了分配内存和赋值操作,但还未完成初始化。
      2. 此时,instance 已经不为 null,但它指向的对象尚未完全初始化。
    1. 如果线程 B 此时调用 getInstance(),判断 instance != null 为真,但实际访问的是一个未初始化完全的对象,这将导致程序出错。
    2. 加上 volatile 后,禁止指令重排序,确保初始化顺序正确。
  1. 保证变量的可见性
    1. Java 的内存模型中,每个线程有自己的工作内存。一个线程对变量的修改,可能不会立即被其他线程所见。
    2. 加上 volatile 后,保证每次对 instance读操作都能获取到最新的值
      1. 当线程 A 完成 instance 初始化后,其他线程(如 B 线程)立刻可见,而不会读取到旧值或中间状态。

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 修饰,保证只会被实例化一次,并且严格保证实例化顺序。

优点:

  • 线程安全
  • 实现了延迟加载,按需初始化

4. ⭐枚举单例(最安全,推荐)

// 枚举单例
public enum Singleton {
    INSTANCE;
}

优点:

  • 简单
  • 天然防止反射和序列化破坏单例

破坏单例

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;
}

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
    }
}

原因

  • 反射可以访问私有构造方法并直接调用,从而绕过单例模式的限制。

解决方案

  1. 在构造方法中防止重复实例化
private static boolean isCreated = false;

private Singleton() {
    if (isCreated) {
        throw new RuntimeException("Singleton instance already created!");
    }
    isCreated = true;
}
  1. 使用枚举单例
    枚举类的单例天然防止反射和序列化破坏。
public enum Singleton {
    INSTANCE;
}

3. 总结

  • 序列化破坏:通过 readResolve() 方法解决。
  • 反射破坏:通过构造方法检查或使用枚举单例解决。
  • 推荐方式:使用 枚举单例,最简单且最安全,能有效防止这两种破坏。

在源码中的应用

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
    }
}

特点

  • 全局唯一实例。
  • 使用饿汉式,保证线程安全。

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
    }
}

特点

  • 使用同步方法保证线程安全。
  • 懒加载,实例在需要时创建。

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 是唯一的。

4. 总结

在 JDK 源码中,单例模式被广泛应用于需要 全局唯一实例资源共享 的场景:

  1. 饿汉式Runtime 类。
  2. 懒汉式Desktop 类。
  3. 组合模式Logger 类中的 LogManager 单例。

这些设计的核心目标是:确保全局状态的一致性、节省资源以及简化管理

单例模式优缺点:


与其他模式的关系:

  1. 外观模式类通常可以转换为单例模式类, 因为在大部分情况下一个外观对象就足够了。
  2. 如果你能将对象的所有共享状态简化为一个享元对象, 那么享元模式就和单例类似了。 但这两个模式有两个根本性的不同。
    1. 只会有一个单例实体, 但是享元类可以有多个实体, 各实体的内在状态也可以不同。
    2. 单例对象可以是可变的。 享元对象是不可变的。
  1. 抽象工厂模式生成器模式原型模式都可以用单例来实现。
相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
3天前
|
设计模式 存储 算法
「全网最细 + 实战源码案例」设计模式——命令模式
命令模式(Command Pattern)是一种行为型设计模式,将请求封装成独立对象,从而解耦请求方与接收方。其核心结构包括:Command(命令接口)、ConcreteCommand(具体命令)、Receiver(接收者)和Invoker(调用者)。通过这种方式,命令的执行、撤销、排队等操作更易扩展和灵活。 适用场景: 1. 参数化对象以操作。 2. 操作放入队列或远程执行。 3. 实现回滚功能。 4. 解耦调用者与接收者。 优点: - 遵循单一职责和开闭原则。 - 支持命令组合和延迟执行。 - 可实现撤销、恢复功能。 缺点: - 增加复杂性和类数量。
33 14
「全网最细 + 实战源码案例」设计模式——命令模式
|
3天前
|
设计模式 存储 Java
「全网最细 + 实战源码案例」设计模式——责任链模式
责任链模式(Chain of Responsibility Pattern)是一种行为型设计模式,允许将请求沿着处理者链进行发送。每个处理者可以处理请求或将其传递给下一个处理者,从而实现解耦和灵活性。其结构包括抽象处理者(Handler)、具体处理者(ConcreteHandler)和客户端(Client)。适用于不同方式处理不同种类请求、按顺序执行多个处理者、以及运行时改变处理者及其顺序的场景。典型应用包括日志处理、Java Web过滤器、权限认证等。
37 13
「全网最细 + 实战源码案例」设计模式——责任链模式
|
5天前
|
设计模式 算法 开发者
「全网最细 + 实战源码案例」设计模式——策略模式
策略模式(Strategy Pattern)是一种行为型设计模式,用于定义一系列可替换的算法或行为,并将它们封装成独立的类。通过上下文持有策略对象,在运行时动态切换算法,提高代码的可维护性和扩展性。适用于需要动态切换算法、避免条件语句、经常扩展算法或保持算法独立性的场景。优点包括符合开闭原则、运行时切换算法、解耦上下文与策略实现、减少条件判断;缺点是增加类数量和策略切换成本。示例中通过定义抽象策略接口和具体策略类,结合上下文类实现动态算法选择。
43 8
「全网最细 + 实战源码案例」设计模式——策略模式
|
5天前
|
设计模式 SQL 算法
「全网最细 + 实战源码案例」设计模式——模板方法模式
模板方法模式是一种行为型设计模式,定义了算法的骨架并在父类中实现不变部分,将可变部分延迟到子类实现。通过这种方式,它避免了代码重复,提高了复用性和扩展性。具体步骤由抽象类定义,子类实现特定逻辑。适用于框架设计、工作流和相似算法结构的场景。优点包括代码复用和符合开闭原则,缺点是可能违反里氏替换原则且灵活性较低。
49 7
「全网最细 + 实战源码案例」设计模式——模板方法模式
|
2月前
|
设计模式 前端开发 搜索推荐
前端必须掌握的设计模式——模板模式
模板模式(Template Pattern)是一种行为型设计模式,父类定义固定流程和步骤顺序,子类通过继承并重写特定方法实现具体步骤。适用于具有固定结构或流程的场景,如组装汽车、包装礼物等。举例来说,公司年会节目征集时,蜘蛛侠定义了歌曲的四个步骤:前奏、主歌、副歌、结尾。金刚狼和绿巨人根据此模板设计各自的表演内容。通过抽象类定义通用逻辑,子类实现个性化行为,从而减少重复代码。模板模式还支持钩子方法,允许跳过某些步骤,增加灵活性。
128 11
|
3月前
|
设计模式 安全 Java
Kotlin教程笔记(51) - 改良设计模式 - 构建者模式
Kotlin教程笔记(51) - 改良设计模式 - 构建者模式
|
17天前
|
设计模式
「全网最细 + 实战源码案例」设计模式——模式扩展(配置工厂)
该设计通过配置文件和反射机制动态选择具体工厂,减少硬编码依赖,提升系统灵活性和扩展性。配置文件解耦、反射创建对象,新增产品族无需修改客户端代码。示例中,`CoffeeFactory`类加载配置文件并使用反射生成咖啡对象,客户端调用时只需指定名称即可获取对应产品实例。
78 40
|
18天前
|
设计模式 关系型数据库
「全网最细 + 实战源码案例」设计模式——简单工厂模式
简单工厂模式是一种创建型设计模式,通过工厂类根据传入参数创建不同类型的对象,也称“静态工厂方法”模式。其结构包括工厂类、产品接口和具体产品类。优点是封装性强、代码复用性好;缺点是扩展性差,增加新产品时需修改工厂类代码,违反开闭原则。适用于对象种类较少且调用者无需关心创建细节的场景。
50 19
|
5月前
|
设计模式 数据库连接 PHP
PHP中的设计模式:提升代码的可维护性与扩展性在软件开发过程中,设计模式是开发者们经常用到的工具之一。它们提供了经过验证的解决方案,可以帮助我们解决常见的软件设计问题。本文将介绍PHP中常用的设计模式,以及如何利用这些模式来提高代码的可维护性和扩展性。我们将从基础的设计模式入手,逐步深入到更复杂的应用场景。通过实际案例分析,读者可以更好地理解如何在PHP开发中应用这些设计模式,从而写出更加高效、灵活和易于维护的代码。
本文探讨了PHP中常用的设计模式及其在实际项目中的应用。内容涵盖设计模式的基本概念、分类和具体使用场景,重点介绍了单例模式、工厂模式和观察者模式等常见模式。通过具体的代码示例,展示了如何在PHP项目中有效利用设计模式来提升代码的可维护性和扩展性。文章还讨论了设计模式的选择原则和注意事项,帮助开发者在不同情境下做出最佳决策。
|
16天前
|
设计模式 Java
「全网最细 + 实战源码案例」设计模式——生成器模式
生成器模式(Builder Pattern)是一种创建型设计模式,用于分步骤构建复杂对象。它允许用户通过控制对象构造的过程,定制对象的组成部分,而无需直接实例化细节。该模式特别适合构建具有多种配置的复杂对象。其结构包括抽象建造者、具体建造者、指挥者和产品角色。适用于需要创建复杂对象且对象由多个部分组成、构造过程需对外隐藏或分离表示与构造的场景。优点在于更好的控制、代码复用和解耦性;缺点是增加复杂性和不适合简单对象。实现时需定义建造者接口、具体建造者类、指挥者类及产品类。链式调用是常见应用方式之一。
49 12