Java--多线程--单例模式

上篇我们讲解了什么多线程中wait&notify相关的知识点,本篇来讲解多线程中的单例模式~

1. 什么是单例模式?

单例模式就是一种创建设计模式,确保一个类只有一个实例,并提供一个全局访问点。它在很多场景中非常有用,例如配置管理器、线程池、数据库连接池等等。

核心要素:

  • 私有构造函数,防止外部直接实例化。
  • 静态私有成员变量,保存类的唯一实例。
  • 静态公有方法,提供获取该实例的全局访问点。

2. 单例模式的几种实现方式

单例模式具体的实现方式有很多,最常见的就是'饿汉'和'懒汉两种'~

2.1 饿汉模式

饿汉模式的特点是在类加载的时候创建实例。

java 复制代码
/**
 * 单例模式示例类
 * 使用饿汉模式实现单例模式,确保整个应用程序中只有一个实例
 */
class Singleton{
    //通过饿汉模式构造单例模式
    private static Singleton instance = new Singleton();
    public static Singleton getInstatice(){
        return instance;
    }
    private Singleton(){

    }

    public Singleton(int n){

    }

}

public class demo15 {
    public static void main(String[] args) {
        Singleton t1 = Singleton.getInstatice();
        Singleton t2 = Singleton.getInstatice();
        System.out.println(t1==t2);

    }
}
  • private static final Singleton instance = new Singleton();:在类加载时,静态变量 instance 被初始化,创建了单例对象。final 修饰确保引用不可变。

  • private Singleton():私有构造器,阻止外部通过 new 创建实例。

  • 提供的 public Singleton(int n) 构造器是错误的,它会暴露公共构造方法,允许外部创建新实例,破坏单例。在实际编写时,不应提供任何公共构造器。

  • getInstance() 方法直接返回已创建的实例,无需任何判断

通过这个代码案例我们可以看到,一开始就直接创建了一个实例。既然叫饿汉式肯定是有原因的,就像一天没吃饭的人,一启动(类加载)就立刻创建实例(吃东西),不管后面是否真的需要这个实例。

饿汉模式的优点:

  1. 简单直观:实现简单,没有线程安全问题, 因为实例在类加载的时候就已经创建,JVM保证了线程安全(类加载过程是线程互斥的)
  2. 访问速度块:调用 getInstance() 时直接返回,无需判断或者同步。

饿汉模式的缺点:

  1. 可能导致资源浪费:如果程序自始至终没有使用这个创建的实例,那么他就会一直占用这个内存空间。
  2. 无法延迟加载:实例在类加载时就被创建,不能按需创建,如果初始化很耗时,会影响程序启动速度。

类加载的机制是:当类被主动引用时(比如调用静态方法,访问静态变量等) 会出发类加载。饿汉式在类加载的时候创建实例,因此 getInstance() 调用时实例已经存在。

如何避免资源浪费:如果实例创建开销大并且可能不被使用,就不应该使用饿汉式。

也就是说,饿汉式最好使用在单利占用资源少,并且一定会被使用的场景之下。

2.2 懒汉式

2.2.1 线程不安全写法

懒汉式在类加载的时候不创建实例,第一次使用的时候才创建实例。

java 复制代码
class Singleton {
    private static Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton(); // 线程不安全点
        }
        return instance;
    }
}
  • private static Singleton instance;:声明静态变量,但不立即初始化。

  • getInstance() 方法首先判断 instance 是否为 null,如果是,则创建一个新实例并赋值;否则直接返回已有实例。

其实在这个代码中存在一个线程安全的点:

在多线程环境下,当多个线程同时首次调用 getInstance() 时,可能会同时键入 if(instance == null)这个判断,并且都发现 instance 为null,然后各自执行 instance = new Singleton,导致创建多个实例,违反单例原则。

具体场景

  1. 线程 A 进入 if 判断,发现 instance == null,准备创建实例。

  2. 此时 CPU 调度切换到线程 B,线程 B 也进入 if 判断,同样发现 instance == null(因为 A 还没来得及赋值),于是线程 B 创建一个实例。

  3. 线程 A 继续执行,又创建一个实例。最终得到两个不同的实例

优点:

  • 延迟加载:实例在第一次使用时才创建,避免了饿汉式的资源浪费。
  • 实现简单,代码直观

缺点:

  • 线程不安全:不能用于多线程环境
  • 即使在单线程环境下,如果后续引入多线程代码,也可能出现问题,因此不推荐使用。

这个写法最好是不用!!!

2.2.2 线程安全写法

上面的那种写法可能会导致多个线程进入方法中,所以我们需要做的是想办法让同一时刻只有一个线程进入该方法,于是我们可以借出锁的特性加入一个 synchronized 关键字,使该方法成为同步方法,让同一时刻只进入一个线程,从而保证线程安全。

java 复制代码
class Singleton {
    private static Singleton instance;

    private Singleton() {}

    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
  • getInstance() 方法上添加 synchronized 关键字,使该方法成为同步方法。

  • 同一时刻只有一个线程能进入该方法,从而保证了线程安全。

优点:

  • 线程安全:通过同步锁解决了多线程并发创建实例的问题
  • 延迟加载:实例在第一次调用时才创建。

缺点:

  • 性能差:每次调用 getInstance() 都需要获取锁,及时实例已经创建,后续调用仍然要同步。同步会造成线程阻塞和上下文切换,高并发环境下会严重影响性能。
  • 实际上,只有在第一次创建时需要同步,后续的读操作根本不需要同步。

使用场景:

  • 单例对象使用频率不高,且对性能要求不苛刻的场景中,但在实际开发时,我们通常希望更高效的方式。

性能差的原因:锁的获取和释放需要时间,且可能引起线程阻塞。

3. 双重检查锁(DCL)

3.1 代码示例

java 复制代码
class Singleton {
    private static volatile Singleton instance; // volatile 禁止指令重排

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) { // 第一次检查,避免不必要的同步
            synchronized (Singleton.class) {
                if (instance == null) { // 第二次检查,保证只创建一个实例
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

代码详解

  • 第一次检查if (instance == null) 放在同步块外,这样当实例已创建时,直接返回,无需进入同步块,避免了性能损失。

  • 同步块synchronized (Singleton.class) 锁住类对象,保证同一时刻只有一个线程进入创建代码。

  • 第二次检查 :在同步块内再次检查 instance == null,这是因为可能有多个线程同时通过第一次检查,其中一个线程进入同步块创建实例后,其他线程进入同步块时需避免再次创建。

  • volatile 关键字:这是 DCL 的关键,用于禁止指令重排序,防止返回"半初始化"的对象。

3.2 为什么需要 volatile?

instance = new Singleton(); 这一行代码在 JVM 中实际上分解为三步:

  1. 分配内存空间

  2. 调用构造函数初始化对象

  3. 将引用指向分配的内存地址

但 JVM 可能会进行指令重排序 ,将第 2 步和第 3 步的顺序交换(步骤 2 和 3 没有数据依赖,允许重排)。如果发生重排序,可能先执行引用赋值(此时对象还未初始化),然后另一个线程在第一次检查时发现 instance 不为 null,直接返回这个对象。但该对象尚未初始化(可能某些字段还是默认值),程序在使用时就会出错。

volatile 的作用:

  • 禁止指令重排序:保证初始化完成后再将引用赋值。

  • 保证可见性 :一个线程修改 instance 后,其他线程立即可见。

优点

  • 延迟加载,只在第一次创建时同步,后续调用无同步开销,性能好。

  • 线程安全,兼顾了性能和安全。

缺点

  • 代码复杂 ,容易出错(如忘记 volatile)。

  • 在 Java 1.5 之前,volatile 的语义较弱,不能完全保证 DCL 的正确性;从 Java 1.5 开始,volatile 具备了完整的禁止重排序语义,DCL 才可靠。

适用场景

  • 需要延迟加载,且对性能要求较高的场景。

  • 如果不需要延迟加载,饿汉式或静态内部类更简单。

4. 静态内部类

4.1 代码示例

java 复制代码
class Singleton {
    private Singleton() {}

    // 静态内部类
    private static class Holder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return Holder.INSTANCE;
    }
}

代码详解

  • Singleton 类中定义了一个静态内部类 Holder

  • Holder 中有一个静态常量 INSTANCE,在 Holder 类加载时初始化。

  • getInstance() 方法直接返回 Holder.INSTANCE

4.2 实现原理

  • 延迟加载 :外部类 Singleton 加载时,内部类 Holder 并不会被加载。只有当第一次调用 getInstance() 时,虚拟机会加载 Holder 类,并初始化其静态变量 INSTANCE。因此实现了延迟加载。

  • 线程安全 :类加载过程由 JVM 保证线程安全(一个类只会被加载一次,且加载过程中其他线程无法访问)。所以 INSTANCE 的创建是线程安全的。

  • 无同步开销 :不需要 synchronized,性能与饿汉式一样好。

优点

  • 简洁高效:代码简单,没有同步开销。

  • 延迟加载:实例在第一次使用时创建,避免了资源浪费。

  • 线程安全:由类加载机制保证,无需额外同步。

缺点

  • 无法在创建时传递参数 :因为实例是通过静态初始化创建的,无法在运行时传入参数。但单例通常不需要参数,如果需要,可以在 getInstance() 中通过某种配置机制获取。

  • 相比枚举,无法防止反射和序列化破坏(但可以加防护)。

适用场景

  • 需要延迟加载且对性能要求高的场景。这是实际开发中最常用的懒加载单例实现。

静态内部类何时被加载?------ 当内部类被主动使用时才会加载,主动使用包括访问其静态成员。

为什么线程安全?------ JVM 类加载机制保证。

与 DCL 对比:静态内部类更简单、更可靠,没有 volatile 的复杂性。

以上就是本篇的全部内容啦~下篇再见~

相关推荐
随风,奔跑2 小时前
Spring MVC
java·后端·spring
dfafadfadfafa2 小时前
嵌入式C++安全编码
开发语言·c++·算法
计算机安禾2 小时前
【C语言程序设计】第34篇:文件的概念与文件指针
c语言·开发语言·数据结构·c++·算法·visual studio code·visual studio
追风林2 小时前
idea支持本地 的 服务器 远程debug
java·服务器·intellij-idea
凸头2 小时前
AI 流式聊天接口实现(WebFlux+SSE)
java·人工智能
简宸~2 小时前
VS Code + LaTex + SumatraPDF联合使用指南
java·vscode·latex·sumatrapdf
弦有三种苦难2 小时前
CCF-202412-T3缓存模拟90分
java·开发语言·spring
会编程的土豆2 小时前
【数据结构与算法】 二叉树做题
开发语言·数据结构·c++·算法
青槿吖2 小时前
SpringMVC通关秘籍(下):日期转换器、拦截器与文件上传的奇幻冒险
java·开发语言·数据库·sql·mybatis·状态模式