当前位置: 首页 > news >正文

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

文章目录

  • 单例模式(Singleton Pattern)
    • 一、饿汉模式
    • 二、懒汉模式
    • 解决懒汉式线程安全问题
    • 双重校验锁提高并发性能
    • 静态内部类(JDK 1.2+)
  • 最佳方法:枚举方式(JDK 1.5+)
  • 方法的对比

单例模式(Singleton Pattern)

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

一、饿汉模式

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

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

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

classSingleton{privatestaticSingletoninstance=newSingleton();privateSingleton(){}publicstaticSingletongetInstance(){returninstance;}}publicclassDemo{publicstaticvoidmain(String[]args){//Singleton singleton = new Singleton();//errorSingletonsingleton=Singleton.getInstance();}}

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

二、懒汉模式

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

classSingleton{privatestaticSingletoninstance=null;privateSingleton(){}publicstaticSingletongetInstance(){if(instance==null){instance=newSingleton2();}returninstance;}}publicclassDemo{publicstaticvoidmain(String[]args){Singleton2singleton=Singleton.getInstance();}}

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

解决懒汉式线程安全问题

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

classSingleton{privatestaticSingletoninstance=null;privateSingleton(){}publicstaticSingletongetInstance(){synchronized(Singleton.class){if(instance==null){instance=newSingleton();}}returninstance;}}

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

双重校验锁提高并发性能

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

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

classSingleton{publicstaticSingletongetInstance(){if(instance==null){// 1. 先看一眼(无锁)synchronized(Singleton.class){// 2. 发现是空的?才去排队if(instance==null){// 3. 排到了再确认一下instance=newSingleton();}}}returninstance;// 4. 大多数情况直接返回,不碰锁}}

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

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

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

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

  • A线程:刚执行完步骤 3(还没初始化)
  • B线程: 过来看到 instance != null 直接返回了
classSingleton{privatestaticvolatileSingletoninstance=null;//加上 volatile 禁止重排序privateSingleton2(){}publicstaticSingletongetInstance(){if(instance==null){//B线程发现instance != null,但是此时的instance还未初始化synchronized(Singleton.class){if(instance==null){instance=newSingleton();//A线程将引用指向内存地址,但还未初始化对象}}}returninstance;}}

静态内部类(JDK 1.2+)

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

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

// 完美版:不用锁,不用 volatile,JVM 保证线程安全且懒加载publicclassSingleton{privateSingleton(){}privatestaticclassHolder{staticfinalSingletonINSTANCE=newSingleton();}publicstaticSingletongetInstance(){returnHolder.INSTANCE;}}

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

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

publicfinalclassSingletonextendsjava.lang.Enum<Singleton>{publicstaticfinalSingletonINSTANCE=newSingleton();......}

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

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

publicenumSingleton{INSTANCE;// 定义一个枚举元素,它就代表了 Singleton 的一个实例// 可以像普通类一样添加成员变量和成员方法privateStringconfigInfo="default";publicvoiddoSomething(){System.out.println("通过枚举实现单例模式");}publicStringgetConfigInfo(){returnconfigInfo;}}// 使用方式publicclassDemo{publicstaticvoidmain(String[]args){Singletonsingleton=Singleton.INSTANCE;singleton.doSomething();}}

方法的对比

实现方式是否线程安全是否懒加载并发性能反射攻击和序列化破坏
饿汉模式安全非懒加载无法防御
懒汉模式(无锁)不安全懒加载无法防御
懒汉模式(方法加锁)安全懒加载无法防御
双重检查锁 + volatile安全懒加载较好无法防御
静态内部类安全懒加载优秀无法防御
枚举(Enum)安全非懒加载绝对防御
http://www.jsqmd.com/news/621398/

相关文章:

  • 【Python办公】批量文件重命名
  • 查重一次省百元!PaperXie 四大检测系统,本科生论文通关的省钱秘籍
  • Python机器学习框架对比:从理论到实践
  • rag系统落地化
  • 项目介绍 MATLAB实现基于双向长短期记忆网络(BiLSTM)进行锂电池剩余寿命预测的详细项目实例(含模型描述及部分示例代码)专栏近期有大量优惠 还请多多点一下关注 加油 谢谢 你的鼓励是我前行的动
  • 2026年04月10日最热门的开源项目(Github)
  • mysql数据库索引失效的常见原因_分析索引设计与使用误区
  • 查重踩坑退退退!PaperXie 四大查重功能,本科生闭眼过查重关
  • 从CubeMX到AC6:STM32H743的MPU与分散加载文件(.sct)配置避坑全记录(LWIP+FreeRTOS)
  • 怎么为MongoDB事务调优:将读操作尽量移到事务外面执行
  • 2026-04-11 全国各地响应最快的 BT Tracker 服务器(电信版)
  • 2026年新型隔墙板厂家选购指南:预制隔墙板/ALC板材/ALC蒸压加气混凝土条板/ALC隔墙板/GRC轻质隔墙板/选择指南 - 优质品牌商家
  • Programming Fog:面向雾化控制的Arduino轻量级硬件抽象库
  • LangChain模块(四)Chains工作流编排核心
  • Shadow:Advisor 工具,这才是我们要学习的好架构模式
  • 基本数据类型(小数/浮点数)
  • 知识图谱增强的大语言模型推理:从思维链到动态知识融合
  • 鸿蒙 数据库构建查询条件:greaterThan
  • 动态规划之【树形DP】第2课:树形DP应用案例实践1
  • LangChain模块(五)Memory让模型拥有上下文记忆
  • 第2讲:C语言数据类型和变量
  • 鹏哥c语言复习第十一讲----指针1基础概念
  • 查重不用愁!PaperXie 四大检测模块,一站式解决论文重复率 + AIGC 率难题
  • 用confyUI搭建AI动漫工作流 |【小白篇】|【解释】
  • GME-Qwen2-VL-2B-Instruct保姆级教程:Linux服务器后台常驻服务部署方案
  • 2026年名酒回收全解析:选服务商必看的7个核心维度 - 优质品牌商家
  • Shiftbrite LED驱动原理与STM32嵌入式实现
  • LangChain进阶(一)Tools外部能力接入
  • ICC2与Innovus实战:手把手教你搞定Reg2ICG的Setup违例(附PT验证技巧)
  • OpenClaw v2026.4.9 初始化安装推荐“技能包”(Skills)