⛳ 面试题-单例模式会存在线程安全问题吗?

news2024/11/21 11:05:03

🎍目录

  • ⛳ 面试题-单例模式会存在线程安全问题吗?
    • 🎨 一、单例模式-简介
    • 🚜 二、饿汉式
    • 🐾 三、懒汉式
      • 🎯 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();
    这段代码其实是分为三步执行:
    1. 为 instance 分配内存空间;
    2. 初始化 instance ;
    3. 将 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做了特殊的规定,枚举类型序列化和反序列化出来的是同一个对象。

除此之外,枚举还可以防止反射调用。

📢 六、总结

综上,线程安全的几种单例模式比较来看:

枚举(无锁,调用效率高,可以防止反射和反序列化调用,不能延时加载)> 静态内部类(无锁,调用效率高,可以延时加载)> 双重同步锁(有锁,调用效率高于懒汉式,可以延时加载)> 懒汉式(有锁,调用效率不高,可以延时加载)~= 饿汉式(无锁,调用效率高,不能延时加载)

注:只有枚举类型能防止反射和反序列化;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/958053.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于pytorch LSTM 的股票预测

学习记录于《PyTorch深度学习项目实战100例》 https://weibaohang.blog.csdn.net/article/details/127365867?ydrefereraHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzQ3MjU2MTYyL2NhdGVnb3J5XzEyMDM2MTg5Lmh0bWw%2Fc3BtPTEwMDEuMjAxNC4zMDAxLjU0ODI%3D 1.tushare Tushare是一个免费、…

C++项目:网络版本在线五子棋对战

目录 1.项目介绍 2.开发环境 3.核心技术 4. 环境搭建 5.websocketpp 5.1原理解析 5.2报文格式 5.3websocketpp常用接口介绍 5.4websocket服务器 6.JsonCpp使用 6.1Json数据格式 6.2JsonCpp介绍 7.MySQL API 7.1MySQL API介绍 7.2MySQL API使用 7.3实现增删改查…

lnmp架构-mysql2

4.mysql 组复制集群 首先对所有的节点重新初始化 因为对节点的数据一致性要求非常高 主从复制的时候 slave只会复制master的binlog日志 就是二进制日志 不会复制relay_log 在server1上 根据实际情况修改主机名和网段 log_slave_updateON 意思就是 当slave的sql线程做完之后…

深度学习基础篇 第一章:卷积

dummy老弟这几天在复习啊我也跟着他重新复习一轮。 这次打算学的细一点&#xff0c;虽然对工作没什么帮助&#xff0c;但是理论知识也能更扎实吧&#xff01; 从0开始的深度学习大冒险。 参考教程&#xff1a; https://www.zhihu.com/question/22298352 https://zhuanlan.zhih…

k8s 启动和删除pod

k8s创建pod pod的启动流程 流程图 运维人员向kube-apiserver发出指令&#xff08;我想干什么&#xff0c;我期望事情是什么状态&#xff09; api响应命令,通过一系列认证授权,把pod数据存储到etcd,创建deployment资源并初始化。(期望状态&#xff09; controller通过list-wa…

C++信息学奥赛1184:明明的随机数

#include <bits/stdc.h> using namespace std; int main() {int n; // 数组长度cin >> n; // 输入数组长度int arr[n]; // 定义整数数组&#xff0c;用于存储输入的整数// 输入数组元素for (int i 0; i < n; i){cin >> arr[i];}int e 0; // 计数器&…

长胜证券:政策暖风不断 静待春暖花开

长胜证券指出&#xff0c;经济数据的逐步企稳上升&#xff0c;能够提振商场对经济复苏的决心&#xff0c;同时弱复苏布景下&#xff0c;政策的刺激力度也将为商场走强供给良好的土壤。暖风持续发布下&#xff0c;多方力量也在悄然间发生变化&#xff0c;重视权重、金融板块回暖…

docker命令学习

docker vscode插件出现的问题 docker命令 docker images &#xff08;查看所有的镜像&#xff09; docker ps -a &#xff08;查看所有的容器&#xff09; docker ps &#xff08;查看运行的容器&#xff09; docker run imageID docker run --gpus all --shm-size8g -it imag…

什么是数字孪生?

推荐&#xff1a;使用 NSDT场景编辑器 快速搭建3D应用场景 走进一家汽车装配厂。看到工人将螺母逐渐减少到螺栓上。听到气动工具的嗡嗡声。观看原始的车身沿着生产线滑行&#xff0c;机器人卷起零件。 现在&#xff0c;在线启动其 3D 数字孪生。看到动画数字人类在完全相同但数…

大数据学习:kafkaManager功能详解

kafkaManager功能详解 一.添加集群 1.1 常用参数说明 下面已常用的选项作说明 1&#xff09;Enable JMX Polling 是否开启 JMX 轮训&#xff0c;该部分直接影响部分 kafka broker 和 topic 监控指标指标的获取&#xff08;生效的前提是 kafka 启动时开启了 JMX_PORT。主要影…

Vue基础1:生命周期汇总(vue2)

Description 生命周期图&#xff1a; 可以理解vue生命周期就是指vue实例从创建到销毁的过程&#xff0c;在vue中分为9个阶段&#xff1a;创建前/后&#xff0c;载入前/后&#xff0c;更新前/后&#xff0c;销毁前/后&#xff0c;其他&#xff1b;常用的有&#xff1a;created&…

Spring容器及实例化

一、前言 Spring 容器是 Spring 框架的核心部分&#xff0c;它负责管理和组织应用程序中的对象&#xff08;Bean&#xff09;。Spring 容器负责创建、配置和组装这些对象&#xff0c;并且可以在需要时将它们提供给应用程序的其他部分。 Spring 容器提供了两种主要类型的容器&…

【Eclipse】搭建python环境;运行第一个python程序helloword

目录 0.环境 1.需准备&搭建思路 2.搭建具体步骤 1&#xff09;查看是否安装过python 2&#xff09;安装eclipse 3&#xff09;安装和配置pyDev 3.创建第一个python程序具体步骤 1&#xff09;新建项目 2&#xff09;输入项目名字&#xff0c;和配置选项 3&#x…

用户角色权限demo后续出现问题和解决

将demo账号给到理解和蒋老师&#xff0c;测试的时候将登录人账号改了&#xff0c;结果登录不了了&#xff0c;后续还需要分配权限无法更改他人的账号和密码 将用户和权限重新分配&#xff08;数据库更改&#xff0c;不要学我&#xff09; 试着登录还是报一样的错&#xff0c;但…

OA项目之用户登录首页展示

目录 本章节目标&#xff1a;完成OA项目用户登录及首页展示 一.用户登录 User.java UserDao.java IUserDao.java UserAction.java login.jsp&#xff08;登录界面&#xff09; userManage.jsp (数据绑定&#xff0c;修改&#xff0c;删除) userEdit.jsp&#xff08;用…

基于相空间重构的混沌背景下微弱信号检测算法matlab仿真,对比SVM,PSO-SVM以及GA-PSO-SVM

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 4.1 SVM 4.2 PSO-SVM 4.3 GA-PSO-SVM 5.算法完整程序工程 1.算法运行效果图预览 SVM: PSO-SVM: GA-PSO-SVM: 以上仿真图参考文献《基于相空间重构的混沌背景下微弱信号检测方法研究》 2.…

AAC和ADTS音频格式解析

1.ADTS是个啥 ADTS全称是(Audio Data Transport Stream),是AAC的一种十分常见的传输格式。 记得第一次做demux的时候,把AAC音频的ES流从FLV封装格式中抽出来送给硬件解码器时,不能播;保存到本地用pc的播放器播时,我靠也不能播。当时崩溃了,后来通过查找资料才知道。一般…

加速关断BJT开关电路

引言&#xff1a;BJT从导通到关闭存在一定的延时&#xff0c;在特定的场景中比如BJT电平转换&#xff0c;高频信号调理&#xff0c;这种延时存在很大的隐患&#xff0c;本节简述如何消除BJT的关断延时。 €1.延时的产生机理 类似于图15-1&#xff0c;晶体管从截止状态切换到导…

干货| ICML2023:作为自适应自进化规划器的扩散模型

点击蓝字 关注我们 AI TIME欢迎每一位AI爱好者的加入&#xff01; 作者介绍 梁志烜 香港大学计算机系直博一年级学生&#xff0c;导师为罗平教授&#xff0c;研究兴趣是生成式机器学习&#xff0c;Embodied AI和Data-centric learning。 报告题目 作为自适应自进化规划器的扩散…

本地部署体验LISA模型(LISA≈图像分割基础模型SAM+多模态大语言模型LLaVA)

GitHub地址&#xff1a;https://github.com/dvlab-research/LISA 该项目论文paper reading&#xff1a;https://blog.csdn.net/Transfattyacids/article/details/132254770 在GitHub上下载源文件&#xff0c;进入下载的文件夹&#xff0c;打开该地址下的命令控制台&#xff0c;…