⛳ 面试题-单例模式会存在线程安全问题吗?
🎍目录
- ⛳ 面试题-单例模式会存在线程安全问题吗?
- 🎨 一、单例模式-简介
- 🚜 二、饿汉式
- 🐾 三、懒汉式
- 🎯 3.1、懒汉式:在调用 getInstance 的时候才创建对象。(线程不安全)
- 🏓 3.2、改造1:对懒汉式进行加锁改造(线程安全)
- 🔮 3.3、改造2:对懒汉式继续改造。(线程不安全)
- 🏀 3.4、改造3(改造成功):对懒汉式再次改造。(线程安全)
- 3.5、总结
- 🏭 四、内部静态类
- 🧰 4.1、反射攻击
- 🧬 4.2、反序列化攻击
- ⭐ 五、枚举
- 📢 六、总结
⛳ 面试题-单例模式会存在线程安全问题吗?
答:会出现线程安全问题。
首先,在Java中创建单例实例的方式有:饿汉式、懒汉式、静态内部类、枚举等方式;
饿汉模式是天生线程安全的,饿汉模式在类创建的同时,就创建好了一个静态对象,又因为静态变量只会在类创建时执行一次,所以创建好的实例不会再改变,因此是线程安全的。
懒汉式不是线程安全的,当多并发情形下,可能会多个线程都创建实例,不能保证单例模式,可以改成双重校验锁,既保证了调用效率,又保证了线程安全。
静态内部类的方式相比于懒汉模式的优势是可以延迟加载,因为只有在静态内部类被调用时,JVM才会加载它,同时保证了线程安全和调用效率。
以上三种的创建方式都不能解决,反射、反序列化产生的线程安全问题;
使用枚举天然的防止反射和反序列化,既保证了线程安全,又保证了调用效率,但是不能延时加载;
🎨 一、单例模式-简介
单例模式是 Java 中常用的设计模式之一,属于设计模式三大类中的创建型模式。在运行期间,保证某个类仅有一个实例,并提供一个访问它的全局访问点。单例模式所属类的构造方法是私有的,所以单例类是不能被继承的。实现线程安全的单例模式有以下几种方式有:饿汉式、懒汉式、懒汉式改良版(双重同步锁),内部静态类、枚举;
🚜 二、饿汉式
天生是线程安全的 !(但是无法应对反射、序列化的形式)
饿汉式在类创建的同时,就已经创建好了一个静态的对象供系统使用,以后不在改变;
public class Singleton {private static Singleton instance = new Singleton();private Singleton() { // 私有的构造方法}public static Singleton getInstance() {return instance;}}
这是实现一个安全的单例模式的最简单粗暴的写法,所以称之为饿汉式;
因为肚子饿了,想要马上吃到东西,不想等待生产时间。在类被加载的时候就把 Singleton 实例给创建出来,以后不在改变。
饿汉式的优点和缺点:
- 优点:实现简单、线程安全,调用效率高(无锁,且对象在类加载时就已创建,可直接使用);
- 缺点:可能在还不需要此实例的时候就已经把实例创建出来了,不能延时加载(在需要的时候才创建对象)、使用反射,序列化创建对象依然可以不是单例的;
🐾 三、懒汉式
🎯 3.1、懒汉式:在调用 getInstance 的时候才创建对象。(线程不安全)
public class Singleton { private static Singleton instance=null; private Singleton() {}; public static Singleton getInstance(){ if(instance==null){//可能会有多个线程进入代码块,造成实例化多个对象instance=new Singleton(); } return instance; } }
🏓 3.2、改造1:对懒汉式进行加锁改造(线程安全)
public class Singleton { private static Singleton instance=null; private Singleton() {}; public static synchronized Singleton getInstance(){ if(instance==null){ instance=new Singleton(); } return instance; }
}
但是这种方式并不推荐,因为效率想对较低,每个线程在执行 getInstance 的时候都要进行同步。而如果 instance 已经实例化了可以直接返回,还需要进行改造;
🔮 3.3、改造2:对懒汉式继续改造。(线程不安全)
public class Singleton { private static Singleton instance=null; public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { instance = new Singleton(); } } return instance; }
}
这种方式是线程不安全的,假如 A,B 两个线程同时进入到了 if(instance == null)
的代码块, A 线程拿到了锁进入 synchronized
代码块,对 instance 进行实例化,结束并释放锁,B 线程便拿到锁,依然会进入到 synchronized
代码块对 instance 进行实例化。那么这就对 instance 进行了两次实例化。出现了线程安全的问题。
🏀 3.4、改造3(改造成功):对懒汉式再次改造。(线程安全)
这种代码书写方式也称为 双重同步锁
public class Singleton { private static volatile Singleton instance=null; private Singleton(){}public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { //在(3.3)的基础上多了一次判断,避免了线程安全问题if (instance == null) { instance = new Singleton(); }} } return instance; }
}
使用了double-check
即check-加锁-check,减少了同步的开销;
在创建第一个对象时候,可能会有线程1,线程2两个线程进入getInstance()方法,这时对象还未被创建,所以都通过第一层check。接下来的synchronized锁只有一个线程可以进入,假设线程1进入,线程2等待。线程1进入后,由于对象还未被创建,所以通过第二层check并创建好对象,由于对象singleton是被volatile修饰的,所以在对singleton修改后会立即将singleton的值从其工作内存刷回到主内存以保证其它线程的可见性。线程1结束后线程2进入synchronized代码块,由于线程1已经创建好对象并将对象值刷回到主内存,所以这时线程2看到的singleton对象不再为空,因此通过第二层check,最后获取到对象。这里volatile的作用是保证可见性,同时也禁止指令重排序,因为上述代码中存在控制依赖,多线程中对控制依赖进行指令重排序会导致线程不安全。
优点:线程安全,可以延时加载,调用效率比锁加在方法上高。
另外,需要注意 instance采用 volatile 关键字修饰也是很有必要。
- instance采用 volatile 关键字修饰也是很有必要的, instance = new Singleton();
这段代码其实是分为三步执行:- 为 instance 分配内存空间;
- 初始化 instance ;
- 将 instance 指向分配的内存地址;
但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。
使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。
3.5、总结
相比于饿汉式,懒汉式显得没那么 “饿”,在真正需要的时候在去创建实例。
懒汉式的优点和缺点:
- 优点:线程安全的,可以延时加载。
- 缺点:调用效率不高(有锁,且需要先创建对象)、使用反射,序列化创建对象依然可以不是单例的
🏭 四、内部静态类
public class Singleton {private Singleton() {}public static Singleton getInstance() {return SingletonFactory.instance;}private static class SingletonFactory {private static Singleton instance = new Singleton();}
}
静态内部类只有被主动调用的时候,JVM才会去加载这个静态内部类。外部类初次加载,会初始化静态变量、静态代码块、静态方法,但不会加载内部类和静态内部类。
优点:线程安全,调用效率高,可以延时加载。
似乎静态内部类看起来已经是最完美的方法了,其实不是,可以还存在反射攻击和反序列化攻击。
🧰 4.1、反射攻击
public class Singleton {private Singleton() {}public static Singleton getInstance() {return SingletonFactory.instance;}private static class SingletonFactory {private static Singleton instance = new Singleton();}public static void main(String[] args) throws Exception {Singleton singleton = Singleton.getInstance();Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();constructor.setAccessible(true);Singleton newSingleton = constructor.newInstance();System.out.println(singleton == newSingleton);
}}
运行结果:false
通过结果看,这两个实例不是同一个,违背了单例模式的原则;
🧬 4.2、反序列化攻击
<dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId><version>3.8.1</version>
</dependency>
这个依赖提供了序列化和反序列化工具类。
Singleton 类实现了 java.io.Serializable
接口。
public class Singleton implements Serializable {private static class SingletonHolder {private static Singleton instance = new Singleton();}private Singleton() {}public static Singleton getInstance() {return SingletonHolder.instance;}public static void main(String[] args) {Singleton instance = Singleton.getInstance();byte[] serialize = SerializationUtils.serialize(instance); // 序列化为一个数组Singleton newInstance = SerializationUtils.deserialize(serialize); // 通过刚才序列化的数组,进行反序列化System.out.println(instance == newInstance);}}
输出结果:false
,表示不是一个实例;
如果要解决 Singleton 类的实力在序列化和反序列化过程中仍然是唯一的,需要添加一个readResolve()
方法到 Singleton 类中,以便在反序列化是返回相同的实例。如:
private Object readResolve() throws ObjectStreamException {return getInstance();
}
在 Java 中,readResolve()
方法是一个特殊的方法,用于在对象反序列化过程中控制返回的实例。它主要用于解决单例模式在反序列化时可能出现的问题。
⭐ 五、枚举
最佳的单例实现模式就是枚举模式。写法简单,线程安全,调用效率高,可以天然的防止反射和反序列化调用,不能延时加载。
public enum Singleton {INSTANCE;public void doSomething() {System.out.println("doSomething");}
}
调用方法:
public class Main {public static void main(String[] args) {Singleton.INSTANCE.doSomething();}}
直接通过Singleton.INSTANCE.doSomething()的方式调用即可。
枚举如何实现线程安全?反编译后可以发现,会通过一个类去继承该枚举,然后通过静态代码块的方式在类加载时实例化对象,与饿汉类似。https://blog.csdn.net/wufaliang003/article/details/81395411
如何做到防止反序列化调用?每一个枚举类型及其定义的枚举变量在JVM中都是唯一的,Java做了特殊的规定,枚举类型序列化和反序列化出来的是同一个对象。
除此之外,枚举还可以防止反射调用。
📢 六、总结
综上,线程安全的几种单例模式比较来看:
枚举(无锁,调用效率高,可以防止反射和反序列化调用,不能延时加载)> 静态内部类(无锁,调用效率高,可以延时加载)> 双重同步锁(有锁,调用效率高于懒汉式,可以延时加载)> 懒汉式(有锁,调用效率不高,可以延时加载)~= 饿汉式(无锁,调用效率高,不能延时加载)
注:只有枚举类型能防止反射和反序列化;
相关文章:
⛳ 面试题-单例模式会存在线程安全问题吗?
🎍目录 ⛳ 面试题-单例模式会存在线程安全问题吗?🎨 一、单例模式-简介🚜 二、饿汉式🐾 三、懒汉式🎯 3.1、懒汉式:在调用 getInstance 的时候才创建对象。(线程不安全)&…...

C - 滑动窗口 /【模板】单调队列
Description 有一个长为 n 的序列 a,以及一个大小为 k 的窗口。现在这个从左边开始向右滑动,每次滑动一个单位,求出每次滑动后窗口中的最大值和最小值。 例如: The array is [1,3,−1,−3,5,3,6,7] and k3。 Input 输入一共有…...

工厂人员作业行为动作识别检测算法
工厂人员作业行为动作识别检测算法通过yolov7python深度学习算法框架模型,工厂人员作业行为动作识别检测算法实时识别并分析现场人员操作动作行为是否符合SOP安全规范流程作业标准,如果不符合则立即抓拍告警提醒。Python是一种由Guido van Rossum开发的通…...

【数据结构】顺序表详解
当我们写完通讯录后,顺序表肯定难不倒你,跟着小张一起来学习顺序表吧! 线性表 线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表&#x…...

HTML 播放器效果
效果图 实现代码 <!DOCTYPE HTML> <html><head><title>爱看动漫社区 | 首页 </title><link href"css/bootstrap.css" relstylesheet typetext/css /><!-- jQuery --><script src"js/jquery-1.11.0.min.js"…...
C++常用23种设计模式总结(三)------装饰模式
往期回顾 C常用23种设计模式总结(一)------单例模式 C常用23种设计模式总结(二)------观察者模式 什么是装饰模式 装饰模式是一种结构型设计模式,它允许你在运行时为对象动态添加新的行为。该模式通过将对象放入包装器中来实现这一点,这个包装器会实现与…...

选择O型圈时要考虑哪些因素?
为您的应用选择正确的O型圈对于确保适当的密封和较佳性能至关重要。O型圈可用的材料和尺寸多种多样,做出正确的选择可能需要知道一些重要的知识点。在本文中,我们将讨论选择O型圈时需要考虑的一些关键因素。 1、材料兼容性:先要考虑的因素是…...
安全管理中心技术测评要求项
1.系统管理-通过系统管理员进行系统管理操作 1-0/2-2/3-2/4-2 a)对系统管理员进行身份鉴别,只允许其通过特定的命令或操作界面进行系统管理操作,并对这些操作进行审计 b)通过系统管理员对系统的资源和运行进行配置、控制和管理&am…...

Hibernate(Spring Data)抓取策略
文章目录 示例代码放到最后,使用的是Springboot 项目1. 简介2. Hibernate抓取策略分类2.1 即时加载(Eager Loading)2.2 延迟加载(Lazy Loading)2.3 子查询加载(Subselect Loading)2.4 基于批处理…...

【高阶数据结构】map和set的介绍和使用 {关联式容器;键值对;map和set;multimap和multiset;OJ练习}
map和set的介绍和使用 一、关联式容器 关联式容器和序列式容器是C STL中的两种不同类型的容器。 关联式容器是基于键值对的容器,其中每个元素都有一个唯一的键值,可以通过键值来访问元素。关联式容器包括set、multiset、map和multimap。 序列式容器是…...

系统架构技能之设计模式-单件模式
一、开篇 其实我本来不是打算把系统架构中的一些设计模式单独抽出来讲解的,因为很多的好朋友也比较关注这方面的内容,所以我想通过我理解及平时项目中应用到的一 些常见的设计模式,拿出来给大家做个简单讲解,我这里只是抛砖引玉,…...

Redis进阶 - JVM进程缓存
原文首更地址,阅读效果更佳! Redis进阶 - JVM进程缓存 | CoderMast编程桅杆https://www.codermast.com/database/redis/redis-advance-jvm-process-cache.html 传统缓存的问题 传统的缓存策略一般是请求到达 Tomcat 后,先查询 Redis &…...
SD-WAN带您告别高成本、单一功能和安全性差
现今,随着企业规模不断扩大和分散办公越来越普遍,企业对于网络的需求也变得越来越高。然而,传统的组网方式面临着很多的问题,比如:成本高、功能单一、安全性差等问题。 传统组网方式有哪些? 传统的组网方式…...

面试必备:揭秘ArrayList和LinkedList,区别、优缺点与使用场景
大家好,我是你们的小米!今天我要跟大家聊一个在面试中经常被问到的热门话题——ArrayList和LinkedList的区别、优缺点以及它们的使用场景。作为程序员,掌握这些知识点不仅可以在面试中脱颖而出,还能帮助我们更好地在项目中选择合适…...

【局部活动轮廓】使用水平集方法实现局部活动轮廓方法研究(Matlab代码实现)
💥💥💞💞欢迎来到本博客❤️❤️💥💥 🏆博主优势:🌞🌞🌞博客内容尽量做到思维缜密,逻辑清晰,为了方便读者。 ⛳️座右铭&a…...
Git 同步远程新的同名分支
背景 因为远程分支的提交记录过多,导致本地的commit内容过大,会产生一些问题: 第一次拉取时间较长占用本地和远程的存储 原因 因为项目已有一些年头,若是每次文件提交比较大,那么占用空间就更大 解决方案 该方案…...
PingCode DevOps 团队:企业CICD流水线可能会遇到的问题及解法
CICD 流水线是指一系列自动化的构建、测试和部署步骤,用于将应用程序从开发到生产环境的过程。在 CICD 流水线中,每个步骤都是自动化的,并且在完成后会触发下一个步骤的执行。 CICD 的价值 CICD 流水线可以帮助团队更快地交付产品ÿ…...

【LeetCode题目详解】第九章 动态规划part01 509. 斐波那契数 70. 爬楼梯 746. 使用最小花费爬楼梯 (day38补)
本文章代码以c为例! 一、力扣第509题:斐波那契数 题目: 斐波那契数 (通常用 F(n) 表示)形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:…...

图像处理 信号处理板 设计原理图:367-基于zynq XC7Z100 FMC接口通用计算平台
基于zynq XC7Z100 FMC接口通用计算平台 一、板卡概述 板卡由SoC XC7Z100-2FFG900I芯片来完成卡主控及数字信号处理,XC7Z100内部集成了两个ARM Cortex-A9核和一个kintex 7的FPGA,通过PL端FPGA扩展FMC、光纤、IO等接口,PS端ARM扩展网络、USB、R…...
PHP中header()的七种用法
我们在实际开发中经常使用header()实现一些功能,这篇文章介绍关于header()的7中用法,需要的伙伴的开参考一下。 PHP header()的7中用法: 1、跳转页面 可以使用header()实现跳转页面功能。 header(Location:.$url); // $url 跳转页面的地址…...

【大模型RAG】拍照搜题技术架构速览:三层管道、两级检索、兜底大模型
摘要 拍照搜题系统采用“三层管道(多模态 OCR → 语义检索 → 答案渲染)、两级检索(倒排 BM25 向量 HNSW)并以大语言模型兜底”的整体框架: 多模态 OCR 层 将题目图片经过超分、去噪、倾斜校正后,分别用…...

在WSL2的Ubuntu镜像中安装Docker
Docker官网链接: https://docs.docker.com/engine/install/ubuntu/ 1、运行以下命令卸载所有冲突的软件包: for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done2、设置Docker…...
JVM暂停(Stop-The-World,STW)的原因分类及对应排查方案
JVM暂停(Stop-The-World,STW)的完整原因分类及对应排查方案,结合JVM运行机制和常见故障场景整理而成: 一、GC相关暂停 1. 安全点(Safepoint)阻塞 现象:JVM暂停但无GC日志,日志显示No GCs detected。原因:JVM等待所有线程进入安全点(如…...

Unsafe Fileupload篇补充-木马的详细教程与木马分享(中国蚁剑方式)
在之前的皮卡丘靶场第九期Unsafe Fileupload篇中我们学习了木马的原理并且学了一个简单的木马文件 本期内容是为了更好的为大家解释木马(服务器方面的)的原理,连接,以及各种木马及连接工具的分享 文件木马:https://w…...

【分享】推荐一些办公小工具
1、PDF 在线转换 https://smallpdf.com/cn/pdf-tools 推荐理由:大部分的转换软件需要收费,要么功能不齐全,而开会员又用不了几次浪费钱,借用别人的又不安全。 这个网站它不需要登录或下载安装。而且提供的免费功能就能满足日常…...

接口自动化测试:HttpRunner基础
相关文档 HttpRunner V3.x中文文档 HttpRunner 用户指南 使用HttpRunner 3.x实现接口自动化测试 HttpRunner介绍 HttpRunner 是一个开源的 API 测试工具,支持 HTTP(S)/HTTP2/WebSocket/RPC 等网络协议,涵盖接口测试、性能测试、数字体验监测等测试类型…...

java高级——高阶函数、如何定义一个函数式接口类似stream流的filter
java高级——高阶函数、stream流 前情提要文章介绍一、函数伊始1.1 合格的函数1.2 有形的函数2. 函数对象2.1 函数对象——行为参数化2.2 函数对象——延迟执行 二、 函数编程语法1. 函数对象表现形式1.1 Lambda表达式1.2 方法引用(Math::max) 2 函数接口…...

Python环境安装与虚拟环境配置详解
本文档旨在为Python开发者提供一站式的环境安装与虚拟环境配置指南,适用于Windows、macOS和Linux系统。无论你是初学者还是有经验的开发者,都能在此找到适合自己的环境搭建方法和常见问题的解决方案。 快速开始 一分钟快速安装与虚拟环境配置 # macOS/…...

李沐--动手学深度学习--GRU
1.GRU从零开始实现 #9.1.2GRU从零开始实现 import torch from torch import nn from d2l import torch as d2l#首先读取 8.5节中使用的时间机器数据集 batch_size,num_steps 32,35 train_iter,vocab d2l.load_data_time_machine(batch_size,num_steps) #初始化模型参数 def …...
RLHF vs RLVR:对齐学习中的两种强化方式详解
在语言模型对齐(alignment)中,强化学习(RL)是一种重要的策略。而其中两种典型形式——RLHF(Reinforcement Learning with Human Feedback) 与 RLVR(Reinforcement Learning with Ver…...