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

🎍目录

  • ⛳ 面试题-单例模式会存在线程安全问题吗?
    • 🎨 一、单例模式-简介
    • 🚜 二、饿汉式
    • 🐾 三、懒汉式
      • 🎯 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做了特殊的规定,枚举类型序列化和反序列化出来的是同一个对象。

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

📢 六、总结

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

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

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【SpringCloud】SpringCloud整合openFeign

文章目录 前言1. 问题分析2. 了解Feign3. 项目整合Feign3.1 引入依赖3.2 添加注解3.3 编写Feign客户端3.4 测试3.5 总结 4. 自定义配置4.1 配置文件方式4.2 Java代码方式 5. Feign使用优化5.1 引入依赖5.2 配置连接池 6. Feign最佳实践6.1 继承方式6.2 抽取方式 前言 微服务远…

OpenCV

文章目录 OpenCV学习报告读取图片和网络摄像头1.1 图片读取1.2 视频读取1.1.1 读取视频文件1.1.2读取网络摄像头 OpenCV基础功能调整、裁剪图像3.1 调整图像大小3.2 裁剪图像 图像上绘制形状和文本4.1 图像上绘制形状4.2图像上写文字 透视变换图像拼接颜色检测轮廓检测人脸检测…

Spring Boot集成MyBatis Plus

文章目录 一、前言二、步骤2.1、步骤 1&#xff1a;创建 Spring Boot 项目2.2、添加依赖2.2.1、基本的Spring和Spring MVC功能2.2.2、MySQL驱动依赖2.2.3、 MyBatis Plus 的依赖 2.3、配置数据库连接2.4、创建实体类2.5、创建 Mapper 接口2.6、编写 Service 层2.7、编写 Contro…

ubuntu20.04+ROS noetic在线运行单USB双目ORB_SLAM

双目摄像头主要有以下几种&#xff0c;各有优缺点。 1.单USB插口&#xff0c;左右图像单独输出2.双USB插口&#xff0c;左右图像单独输出&#xff08;可能存在同步性问题&#xff09;3.双USB插口&#xff0c;左右图像合成输出4.单USB插口&#xff0c;左右图像合成输出 官方版…

XSS漏洞及分析

目录 1.什么是xss漏洞 1&#xff09;存储型XSS漏洞 2&#xff09;反射型XSS漏洞 3&#xff09;DOM型XSS漏洞 2.什么是domcobble破环 3.案例一 1&#xff09;例题链接 2&#xff09;代码展示 3&#xff09;例题分析 4.案例二 1&#xff09;例题链接 2&#xff09;代…

Java-泛型

文章目录 Java泛型什么是泛型&#xff1f;在哪里使用泛型&#xff1f;设计出泛型的好处是什么&#xff1f;动手设计一个泛型泛型的限定符泛型擦除泛型的通配符 结论 Java泛型 什么是泛型&#xff1f; Java泛型是一种编程技术&#xff0c;它允许在编译期间指定使用的数据类型。…

重磅OpenAI发布ChatGPT企业版本

8月29日凌晨&#xff0c;Open AI官网发布ChatGPT企业版本&#xff01; 企业版简介&#xff1a; ChatGPT企业版提供企业级安全和隐私、无限的高速 GPT-4 访问、用于处理更长输入的更长上下文窗口、高级数据分析功能、自定义选项等等。人工智能可以协助和提升我们工作生活的各个…

魏副业而战:他全职网络创业了

我是魏哥&#xff0c;与其躺平&#xff0c;不如魏副业而战&#xff01; 社群小X今年全职创业&#xff0c;加入社群一个月&#xff0c;就直接做了徒弟&#xff0c;并且进入合伙人的团队。 玩社群&#xff0c;就要做核心。 小X不到两个月就做到了。 看来他蛮有悟性的。 吃过…

Redis的数据结构与单线程架构

"飞吧&#xff0c;去寻觅红色的流星" Redis中的五种数据结构和编码 Redis是一种通过键值对关系存储数据的软件&#xff0c;在前一篇中&#xff0c;我们可以使用type命令实际返回当前键所对应的数据结构类型&#xff0c;例如: String\list\hash\set等等。 但…

R语言入门——line和lines的区别

目录 0 引言一、 line()二、 lines() 0 引言 首先&#xff0c;从直观上看&#xff0c;lines比line多了一个s&#xff0c;但它们还是有很大的区别的&#xff0c;下面将具体解释这个两个函数的区别。 一、 line() 从R语言的帮助文档中找到&#xff0c;line()的使用&#xff0c…

学习笔记230827--vue项目中,子组件拿不到父组件异步获取数据的问题

&#x1f9cb; 问题描述 父组件的数据是请求后台所得&#xff0c;因为是异步数据&#xff0c;就会出现&#xff0c;父组件的值传递过去了&#xff0c;子组件加载不到&#xff0c;拿不到值的问题。 下面从同步数据传递和异步数据传递开始论述问题 &#x1f9cb;&#x1f9cb;1…

【逻辑回归-银行客户】

逻辑回归&#xff1a;从理论到实践 在本文中&#xff0c;我们将介绍一种被广泛用于二分类问题的机器学习模型——逻辑回归。我们将通过一个实例&#xff0c;深入解析如何在 Python 环境中实现逻辑回归。 源数据下载链接 1. 什么是逻辑回归&#xff1f; 逻辑回归是一种用于解…

华为 连接OSPF和RIP网络---OSPF和RIP网络相互引入

路由引入简介 不同路由协议之间不能直接共享各自的路由信息&#xff0c;需要依靠配置路由的引入来实现。 获得路由信息一般有3种途径&#xff1a;直连网段、静态配置和路由协议。可以将通过这3种途径获得的路由信息引入到路由协议中&#xff0c;例如&#xff0c;把直连网段引入…

【python爬虫】9.带着小饼干登录(cookies)

文章目录 前言项目&#xff1a;发表博客评论post请求 cookies及其用法session及其用法存储cookies读取cookies复习 前言 第1-8关我们学习的是爬虫最为基础的知识&#xff0c;从第9关开始&#xff0c;我们正式打开爬虫的进阶之门&#xff0c;学习爬虫更多的精进知识。 在前面几…

Databricks 入门之sql(二)常用函数

1.类型转换函数 使用CAST函数转换数据类型&#xff08;可以起别名&#xff09; SELECTrating,CAST(timeRecorded as timestamp) FROMmovieRatings; 支持的数据类型有&#xff1a; BIGINT、BINARY、BOOLEAN、DATE 、DECIMAL(p,s)、 DOUBLE、 FLOAT、 INT、 INTERVAL interva…

Linux服务器部署JavaWeb后端项目

适用于&#xff1a;MVVM前后台分离开发、部署、域名配置 前端&#xff1a;Vue 后端&#xff1a;Spring Boot 这篇文章只讲后端部署&#xff0c;前端部署戳这里 目录 Step1&#xff1a;服务器上搭建后端所需环境1、更新服务器软件包2、安装JDK83、安装MySQL4、登录MySQL5、修…

Hadoop Yarn 核心调优参数

文章目录 测试集群环境说明Yarn 核心配置参数1. 调度器选择2. ResourceManager 调度器处理线程数量设置3. 是否启用节点功能的自动检测设置4. 是否将逻辑处理器当作物理核心处理器5. 设置物理核心到虚拟核心的转换乘数6. 设置 NodeManager 使用的内存量7. 设置 NodeManager 节点…

「操作系统」1. 基础

前言&#xff1a;操作系统基础八股文 文章目录 一 、操作系统基础1.1 什么是操作系统&#xff1f;1.2 什么是系统调用1.3 什么是中断 &#x1f680; 作者简介&#xff1a;作为某云服务提供商的后端开发人员&#xff0c;我将在这里与大家简要分享一些实用的开发小技巧。在我的职…

Matlab(画图进阶)

目录 大纲 1.特殊的Plots 1.1 loglog(双对数刻度图) ​1.3 plotyy(创建具有两个y轴的图形) 1.4yyaxis(创建具有两个y轴的图) 1.5 bar 3D条形图(bar3) 1.6 pie(饼图) 3D饼图 1.7 polar 2.Stairs And Ste阶梯图 3.Boxplot 箱型图和Error Bar误差条形图 3.1 boxplot 3.2 …

sap 一次性供应商 供应商账户组 临时供应商

sap中有一次性供应商这个名词&#xff0c;一次性供应商和非一次性供应商又有什么区别呢&#xff1f; 有如何区分一次性供应商和非一次性供应商呢&#xff1f; 1 区分一次性供应商和非一次性供应商的标志 在供应商的表LFA1中有个字段标示XCPDK&#xff08;一次性科目&#xff…