【JAVA基础面经】线程安全的单例模式

文章目录


单例模式(Singleton Pattern)

单例模式即代码中的某个类 只能有一个实例,不能有多个 。单例模式有两种主要实现方式,饿汉模式和懒汉模式

一、饿汉模式

在类加载阶段就会直接创建唯一实例,步骤如下

  1. 首先使用static创建一个实例,并立即进行实例化
  • 经过static修饰的成员,准确来说应该称为"类成员",修饰为对应的类属性或类方法
    一个JAVA程序中,一个类对象存在一份(类名.class文件被加载到JVM内存中生成的一个对象),那么类成员(static修饰的成员)也存在一份。
  1. 为了防止再重新new一个实例,需要把构造方法设置成private

  2. 提供能拿到唯一实例的方法

java 复制代码
class Singleton{
    private static Singleton instance = new Singleton();
    private Singleton(){
    }
    public static Singleton getInstance(){
        return instance;
    }
}
public class Demo {
    public static void main(String[] args) {
        //Singleton singleton = new Singleton();//error
        Singleton singleton = Singleton.getInstance();
    }
}

线程安全看多个线程同时调用getInstance()方法时是否会出现错误,饿汉模式中的getInstance仅读取变量内容,如果多个线程同时读一个变量,此时线程是安全的

二、懒汉模式

懒汉模式不会立即初始化实例,而是等到使用的时候再创建。在饿汉模式的基础上进行了修改

java 复制代码
class Singleton{
    private static Singleton instance = null;
    private Singleton(){
    }
    public static Singleton getInstance(){
        if(instance == null){
            instance = new Singleton2();
        }
        return instance;
    }
}
public class Demo {
    public static void main(String[] args) {
        Singleton2 singleton = Singleton.getInstance();
    }
}

懒汉模式中的getInstance()方法既包含了读操作,又包含了修改操作,是非原子性的,可能导致实例被创建出多份(两个线程同时读为null,同时创建新的实例),存在线程不安全问题

解决懒汉式线程安全问题

(1)synchronized加锁:可以通过加锁将操作打包成原子的来保障线程安全,这里的类对象作为锁对象

java 复制代码
class Singleton{
    private static Singleton instance = null;
    private Singleton(){
    }
    public static Singleton getInstance(){
        synchronized (Singleton.class){
            if(instance == null){
                instance = new Singleton();
            }
        }
        return instance;
    }
}

线程不安全是发生在instance被初始化前,未初始化时多线程调用 getInstance 可能同时涉及到读和修改,一旦 instance 初始化之后,仅存在读操作,线程也就安全了。这样初始化后及时线程安全了,每次调用 getInstance 方法都需要进行加锁,从而产生锁竞争的问题。

双重校验锁提高并发性能

(2)加外层 if(双重检查) :对应的改进方案,再添加一个判定条件,让instance初始化之前进行加锁,初始化后就不进行加锁了。里层的 if 条件不能进行省略,在两个 if 条件判断的时间差内,可能存在instance的修改操作,若去掉里层的 if 那么就没有将读写操作进行原子性打包。

此时对象一旦初始化完成,后续所有线程请求都只走最外层那个 if (instance == null),根本不进入 synchronized 块,提高了并发性能。

java 复制代码
class Singleton{
	public static Singleton getInstance() {
	    if (instance == null) {                  // 1. 先看一眼(无锁)
	        synchronized (Singleton.class) {     // 2. 发现是空的?才去排队
	            if (instance == null) {          // 3. 排到了再确认一下
	                instance = new Singleton();
	            }
	        }
	    }
	    return instance;                         // 4. 大多数情况直接返回,不碰锁
	}
}

(3)volatile 修饰防止指令重排序:如果多个线程块都去调用 getInstance 方法,大量的读操作会产生编译器优化,编译器和处理器为了优化性能,可能会改变指令执行顺序

对于下面的代码,new Singleton() 可以分为三个步骤:

  1. 分配内存空间
  2. 初始化对象
  3. 将引用指向内存地址

编译器和 CPU 可能为了优化,将 步骤 2 和步骤 3 交换(单线程下无影响),此时存在线程A、B均调用 getInstance() 方法

  • A线程:刚执行完步骤 3(还没初始化)
  • B线程: 过来看到 instance != null 直接返回了
java 复制代码
class Singleton{
    private static volatile Singleton instance = null;//加上 volatile 禁止重排序
    private Singleton2(){

    }
    public static Singleton getInstance(){
        if(instance == null){				//B线程发现instance != null,但是此时的instance还未初始化
            synchronized (Singleton.class){
                if(instance == null){
                    instance = new Singleton();	//A线程将引用指向内存地址,但还未初始化对象
                }
            }
        }
        return instance;
    }
}

静态内部类(JDK 1.2+)

加 volatile 的双重检查锁已经足够完美,但代码依然略显繁琐且容易写错。静态内部类单例模式利用 JVM 的类加载机制天然保证了线程安全与懒加载,是实际开发中最推荐的写法。

外部类 Singleton 被加载时,其内部的静态内部类 Holder 不会被立即加载。只有当调用 getInstance() 方法,首次访问 Holder.INSTANCE 时,才会触发 Holder 类的加载与初始化

java 复制代码
// 完美版:不用锁,不用 volatile,JVM 保证线程安全且懒加载
public class Singleton {
    private Singleton() {}
    
    private static class Holder {
        static final Singleton INSTANCE = new Singleton();
    }
    
    public static Singleton getInstance() {
        return Holder.INSTANCE;
    }
}

最佳方法:枚举方式(JDK 1.5+)

此时的INSTANCE; 是这个枚举类的唯一一个实例,经过编译器处理后生成了一个 public static final 的常量

java 复制代码
public final class Singleton extends java.lang.Enum<Singleton> {
    public static final Singleton INSTANCE = new Singleton();
    ......
}

枚举实例是在枚举类被加载时初始化的,相当于饿汉模式

对于枚举,Java 在反射 API 底层做了硬性拦截。同时,枚举的序列化机制是 JVM 特殊处理的,不遵循普通类的反序列化逻辑,当反序列化一个枚举对象时,JVM 会根据流中的枚举常量名称(比如 "INSTANCE"),直接返回该枚举类中同名的那个 static final 常量,而不是重新构造一个对象。因此该方法绝对防止多次实例化,天然抵御反射攻击和序列化破坏

java 复制代码
public enum Singleton {
    INSTANCE;   // 定义一个枚举元素,它就代表了 Singleton 的一个实例
    
    // 可以像普通类一样添加成员变量和成员方法
    private String configInfo = "default";
    
    public void doSomething() {
        System.out.println("通过枚举实现单例模式");
    }
    
    public String getConfigInfo() {
        return configInfo;
    }
}

// 使用方式
public class Demo {
    public static void main(String[] args) {
        Singleton singleton = Singleton.INSTANCE;
        singleton.doSomething();
    }
}

方法的对比

实现方式 是否线程安全 是否懒加载 并发性能 反射攻击和序列化破坏
饿汉模式 安全 非懒加载 无法防御
懒汉模式(无锁) 不安全 懒加载 无法防御
懒汉模式(方法加锁) 安全 懒加载 无法防御
双重检查锁 + volatile 安全 懒加载 较好 无法防御
静态内部类 安全 懒加载 优秀 无法防御
枚举(Enum) 安全 非懒加载 绝对防御
相关推荐
汽车仪器仪表相关领域2 小时前
NHXJ-02汽车悬架检验台 实操型实战手册
人工智能·功能测试·测试工具·算法·安全·单元测试·可用性测试
_李小白2 小时前
【OSG学习笔记】Day 39: NodeCallback(帧回调机制)
java·笔记·学习
如来神掌十八式2 小时前
设计模式之装饰器模式
java·设计模式
EasyGBS2 小时前
国密GB35114+国标GB28181平台EasyGBS双标护航为交通视频监控筑牢安全防线
安全
AI_Claude_code2 小时前
安全与合规核心:匿名化、日志策略与法律风险规避
网络·爬虫·python·tcp/ip·安全·http·网络爬虫
cch89182 小时前
C++、Python与汇编语言终极对比
java·开发语言·jvm
2401_832298102 小时前
OpenClaw 4.5 深度解析:从安全硬化到生态重构,AI 执行框架迈入信任时代
人工智能·安全
好家伙VCC2 小时前
**InfluxDB实战进阶:基于Golang的高性能时序数据采集与可视化方
java·开发语言·后端·python·golang
斌味代码2 小时前
Java SpringBoot 微服务实战:企业级架构设计与性能调优完全指南
java·spring boot·微服务