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

单例模式之懒汉式

       在上篇文章中,我们讲了单例模式中的饿汉式,今天接着来讲懒汉式。

1.懒汉式单例模式的实现

public class LazySingleton {private static LazySingleton instance = null;// 让构造函数为private,这样该类就不会被实例化private LazySingleton() {}public static LazySingleton getInstance() {if (instance == null) {instance = new LazySingleton();}return instance;}public long getRamAddress() {return this.hashCode();}
}

      懒汉式的特点在于有需要时才实例化。

2.懒汉式线程安全

      在多线程的环境下,getInstance方法会导致线程不安全。因为在getInstance时,可能A、B两个线程几乎同时进入,在A实例化未完成的情况下,B判断实例仍然为null,因此继续实例化,这样有实例化了两个不同的对象,明显违背了单例的初衷。这里可以稍微验证一下线程安全问题,为了方便验证,在getInstance让当前线程sleep:

    public static LazySingleton getInstance() {if (instance == null) {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}instance = new LazySingleton();}return instance;}

       测试代码:

      /*** 公共方法,在多线程环境下测试单例,避免重复编写测试代码* @param threadCount 线程数* @param func        函数,用于获取单例* @param <T>*/public static <T> void singLetonMultiThread(int threadCount, Supplier<T> func) {ExecutorService executorService = Executors.newFixedThreadPool(threadCount);IntStream.range(0, threadCount).forEach(i -> {executorService.submit(() -> {System.out.println(func.get());});});// 等线程全部执行完后关闭线程池executorService.shutdown();try {executorService.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS);} catch (InterruptedException e) {e.printStackTrace();}}public static void lazySingLetonTest() {Supplier func = () -> LazySingleton.getInstance();singLetonMultiThread(10, func);}

测试结果如下:

  

       十个线程里出现了8个不同的对象!

2.1 双重检查锁

      我们需要改进一下getInstance方法,考虑到效率问题,我们不想直接在getInstance方法上加锁,因为这种方式下每次调用getInstance()时都需要进行线程锁定判断,在多线程高并发访问环境中,将会导致系统性能大大降低。于是,我们得在判断实例是否为空时加锁,并且进行双重检查:

    public static LazySingleton getInstance() {if (instance == null) {synchronized (LazySingleton.class) {// 这里需要再判断一次,因为可能有其它线程已经创建实例if (instance == null) {instance = new LazySingleton();}}}return instance;}

       这样看起来似乎是解决了问题了。

2.2 指令重排

       但这还有瑕疵。对于:

instance = new LazySingleton();

       这个创建对象的语句其实是个非原子操作,在极端的多线程环境下,会存在安全问题。对象的创建过程,在执行的时候分解成以下三条指令:

memory=allocate(); 			//1.分配对象的内存空间
ctorInstance(memory);       //2.执行构造方法来初始化对象
instance=memory;			//3.设置instance指向刚分配的内存地址

       正常执行顺序应该是1->2->3,但可能指令会被重排序为1->3->2,也就是说,2、3步有可能发生指令重排导致重排序,因为synchronized只能保证有序性,但无法禁止指令重排。假设有两个线程A、B,在双重检查锁内,从cpu时间片上的执行顺序如下:

       A线程执行完3还没执行2,虽然分配了内存空间已,但是还没初始化对象,而此时B线程进来判断(instance == null),由于instance已经指向了内存空间,所以instance != null,于是直接返回了对象,但此时对象还未初始化。这样一来,线程B将得到一个还没有被初始化的对象。

       为了防止指令重排,需在声明instance对象时加上volatile关键词:

public class LazySingleton {// volatile,确保本条指令不会因编译器的优化而省略,且要求每次直接读值private static volatile LazySingleton instance = null;// 让构造函数为private,这样该类就不会被实例化private LazySingleton() {//System.out.println("懒汉式单例初始化!");}public static LazySingleton getInstance() {if (instance == null) {synchronized (LazySingleton.class) {// 这里需要再判断一次,因为可能有其它线程已经创建实例if (instance == null) {try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}instance = new LazySingleton();}}}return instance;}public long getRamAddress() {return this.hashCode();}
}

        volatile关键词具备以下功能:
       1. 避免编译器将变量缓存在寄存器里 
       2. 避免编译器调整代码执行的顺序 

       使用volatile声明的变量可以强制屏蔽编译器和JIT的优化工作,能够防止双重检查锁的指令重排。

3.反射破坏单例

      正如在上篇文章饿汉式单例中所说的那样,懒汉式同样有反射的问题。我们采用跟懒汉式一样的方法常识防止破坏单例:

public class LazySingleton {private static volatile LazySingleton instance = null;// 让构造函数为private,这样该类就不会被实例化private LazySingleton() {//System.out.println("懒汉式单例初始化!");synchronized (LazySingleton.class) {if(instance != null){throw new RuntimeException("单例构造器禁止反射调用");}}}public static LazySingleton getInstance() {if (instance == null) {synchronized (LazySingleton.class) {// 这里需要再判断一次,因为可能有其它线程已经创建实例if (instance == null) {instance = new LazySingleton();}}}return instance;}public long getRamAddress() {return this.hashCode();}
}

      再测试一下看看:

    public static void LazySingletonReflectionTest() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {Class<LazySingleton> clazz = LazySingleton.class;// 获取HungrySingLeton的默认构造函数Constructor<LazySingleton> constructor = clazz.getDeclaredConstructor();constructor.setAccessible(true);// 调用默认构造函数创建实例LazySingleton h1 = constructor.newInstance();LazySingleton h2 = constructor.newInstance();System.out.println(h1.getRamAddress());System.out.println(h2.getRamAddress());}

      运行结果:

      没用!因为在类加载的时候,懒汉式单例根本就没有对单例进行初始化,然后反射通过构造函数获取单例,获取的对象都是不同的,所以没法防止反射破坏。当然,如果是先通过getInstance获取单例,再反射,这种情况下就可以防止反射,如:

    public static void reflectionTest2() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {LazySingleton h0 = LazySingleton.getInstance();Class<LazySingleton> clazz = (Class<LazySingleton>) h0.getClass();// 获取HungrySingLeton的默认构造函数Constructor<LazySingleton> constructor = clazz.getDeclaredConstructor();constructor.setAccessible(true);// 调用默认构造函数创建实例LazySingleton h1 = constructor.newInstance();System.out.println(h0.getRamAddress());System.out.println(h1.getRamAddress());}

      懒汉式单例对这个问题没有好的解决办法。

4.优雅的单例实现——枚举

      单例模式存在线程安全、反射、序列化漏洞的问题,虽然可以想办法解决,但代码也比较臃肿了。换个角度,可以用枚举来实现,去规避这些问题。

public enum Singleton {INSTANCE;public void doSomething() {System.out.println("这里实现自己的业务");}
}

4.1 枚举单例防止反射

       先测试一下枚举单例的反射:

    public static void enumSingletonRlectionTest() throws NoSuchMethodException,IllegalAccessException, InvocationTargetException, InstantiationException {Singleton instance1 = Singleton.INSTANCE;Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor(null);declaredConstructor.setAccessible(true);//java.lang.NoSuchMethodException: com.kuang.single.EnumSingle.<init>()Singleton instance2 = declaredConstructor.newInstance();System.out.println(instance1);System.out.println(instance2);}

       结果抛出异常 ,没有构造函数Singleton.<init>():

       这并非防止了反射。枚举Enum是个抽象类,它有个构造函数:

protected Enum(String name, int ordinal) {this.name = name;this.ordinal = ordinal;
}

       我们定义了枚举类单例,实际上就是继承了Enum,所以会有(String.class,int.class)的构造器,测试方法改造下:

    public static void enumSingletonRlectionTest() throws NoSuchMethodException,IllegalAccessException, InvocationTargetException, InstantiationException {Singleton instance1 = Singleton.INSTANCE;Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor(String.class,int.class);declaredConstructor.setAccessible(true);//java.lang.NoSuchMethodException: com.kuang.single.EnumSingle.<init>()Singleton instance2 = declaredConstructor.newInstance();System.out.println(instance1);System.out.println(instance2);}

       结果如下:

       Cannot reflectively create enum objects(无法以反射方式创建枚举对象),可以防止反射,因为反射在通过构造函数的newInstance方法创建对象时,会检查该类是否ENUM修饰,如果是则抛出异常,反射失败。所以枚举类不能通过反射来创建对象

4.2 枚举单例避免序列化问题

       普通的Java类的反序列化过程中,会通过反射调用类的默认构造函数来初始化对象。所以,即使单例中构造函数是私有的,也会被反射给破坏掉。由于反序列化后的对象是重新new出来的,所以这就破坏了单例,必须新增readResolve方法才能防止破坏。

       但是,枚举的反序列化并不是通过反射实现的。在序列化时,Java仅仅是将枚举对象的name属性输出到结果中,反序列化时则通过java.lang.Enum的valueOf(String name) 方法,根据名字查找内存中是否已经有该对象,若找到了就会直接使用它,如果不存在就会抛出异常。同时。编译器不允许任何对这种序列化机制的定制,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。这样一来,序列化的方式就无法创建新的对象了,也就不会发生由于反序列化导致的单例破坏问题。

        实际测试一下:

    public static void enumSingletonSerializable() {ObjectOutputStream oos = null;ObjectInputStream ois = null;try {File file = new File("d:\\Singleton.txt");// -----------序列化-------------// 创建输出流oos = new ObjectOutputStream(new FileOutputStream(file));//将单例对象写到文件中  序列化oos.writeObject(Singleton.INSTANCE);oos.flush();// -----------反序列化-------------// 从文件读取单例对象ois = new ObjectInputStream(new FileInputStream(file));// 反序列化得到对象singLetonSingleton singLeton= (Singleton)ois.readObject();System.out.println(singLeton == Singleton.INSTANCE); //falsefile.deleteOnExit();} catch (Exception e) {e.printStackTrace();} finally {try {if(oos != null) {oos.close();}if(ois != null) {ois.close();}} catch (IOException e) {e.printStackTrace();}}}

     

       测试结果:

4.3 多线程安全

       当我们使用enmu来定义一个枚举类型的时候,编译器会自动帮我们创建一个final类型的类继承Enum类,所以枚举类型不能被继承;同时,编译器所创建的类中,属性和方法也都是都是static类型的,因为static类型的属性会在类被加载之后被初始化。当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的(因为虚拟机在加载枚举的类的时候,会使用ClassLoader的loadClass方法,而这个方法使用同步代码块保证了线程安全)。所以,创建一个enum类型是线程安全的。所以,创建一个enum类型是线程安全的。

       基于前面的分析,单例类的线程安全问题,主要就是单例初始化过程中的线程安全问题。而由于枚举的以上特性,枚举实现的单例是天生线程安全的。所以用枚举实现的单例是最好的方式!

 

参考文章:

两种单例模式详解(内含懒汉式的双重校验锁详解)
为什么我墙裂建议大家使用枚举来实现单例

深度分析Java的枚举类型—-枚举的线程安全性及序列化问题

相关文章:

单例模式之懒汉式

在上篇文章中&#xff0c;我们讲了单例模式中的饿汉式&#xff0c;今天接着来讲懒汉式。 1.懒汉式单例模式的实现 public class LazySingleton {private static LazySingleton instance null;// 让构造函数为private&#xff0c;这样该类就不会被实例化private LazySingleto…...

1638_chdir函数的功能

全部学习汇总&#xff1a;GreyZhang/g_unix: some basic learning about unix operating system. (github.com) 今天看一个半生不熟的小函数&#xff0c;chdir。说半生不熟&#xff0c;是因为这个接口一看就知道是什么功能。然而&#xff0c;这个接口如何用可真就没啥想法了。 …...

使用CEF 获得某头条请求,并生成本地文件的方法

目录 一、获得网站请求响应信息 1、响应过滤 2、匹配过滤URL的函数 3、获得请求响应后的处理...

二十、Django-restframework之视图集和路由器

一、视图集和路由器 REST框架包含了一个处理视图集的抽象&#xff0c;它允许开发人员集中精力建模API的状态和交互&#xff0c;并根据通用约定自动处理URL构造。 视图集类与视图类几乎相同&#xff0c;不同之处在于它们提供的是retrieve或update等操作&#xff0c;而不是get或…...

[深入理解SSD系列 闪存实战2.1.2] SLC、MLC、TLC、QLC、PLC NAND_固态硬盘闪存颗粒类型

闪存最小物理单位是 Cell, 一个Cell 是一个晶体管。 闪存是通过晶体管储存电子来表示信息的。在晶体管上加入了浮动栅贮存电子。数据是0或1取决于在硅底板上形成的浮动栅中是否有电子。有电子为0,无电子为1. SSD 根据闪存颗粒区分,固态硬盘有SLC、MLC、TLC、QLC、PLC 五种类型…...

论文阅读-MGTAB: A Multi-Relational Graph-Based Twitter Account DetectionBenchmark

目录 摘要 1. 引言 2. 相关工作 2.1. 立场检测 2.2.机器人检测 3.数据集预处理 3.1.数据收集和清理 3.2.专家注释 3.3. 质量评估 3.4.特征分析 4. 数据集构建 4.1.特征表示构造 4.2.关系图构建 5. 实验 5.1.实验设置 5.2.基准性能 5.3训练集大小的研究 5.4 社…...

基于libco的c++协程实现(时间轮定时器)

在后端的开发中&#xff0c;定时器有很广泛的应用。 比如&#xff1a; 心跳检测 倒计时 游戏开发的技能冷却 redis的键值的有效期等等&#xff0c;都会使用到定时器。 定时器的实现数据结构选择 红黑树 对于增删查&#xff0c;时间复杂度为O(logn)&#xff0c;对于红黑…...

java多线程与线程池-04线程池与AQS

第7章 线程池与AQS java.util.concurrent包中的绝大多数同步工具,如锁(locks)和屏障(barriers)等,都基于AbstractQueuedSynchronizer(简称AQS)构建而成。这个框架提供了一套同步管理的通用机制,如同步状态的原子性管理、线程阻塞与解除阻塞,还有线程排队等。 在JD…...

优化模型验证关键代码25:样本均值近似技术处理两阶段随机旅行商问题及Gurobipy代码验证

大多数数学规划模型都会考虑到研究问题中存在的不确定性,针对这些不确定性,两种常用的处理方法是鲁棒优化和随机规划。这篇论文我们关注后者,也就是两阶段随机旅行商问题;利用套期保值算法计算不同规模TSP的可行解,同时比较了样本均值近似技术的解的情况,并计算了该问题的…...

老爸:“你做的什么游戏测试简直是不务正业!”——我上去就是一顿猛如虎的解释。

经常有人问我&#xff1a;游戏测试到底是干什么呢&#xff1f;是游戏代练&#xff1f;每天玩游戏&#xff1f;装备随便造&#xff0c;怪物随便秒&#xff0c;线上GM指令随便用&#xff1f;可以每天玩玩游戏&#xff0c;不用忙工作&#xff0c;太爽了&#xff1f;有时朋友不理解…...

JVM垃圾回收调优知识点整理

目录 1、JVM内存模型 1.2、堆及垃圾回收 1.3、JVM参数设置经验: 1.4、对象逃逸分析:...

linux安装mysql-8.0.31

1)、下载mysql-8.0.31压缩包两种方式 a.本地下载后上传服务器解压&#xff0c;下载地址&#xff1a;https://downloads.mysql.com/archives/community/ b.服务器使用命令下载&#xff0c;注意&#xff1a;路径在那&#xff0c;就下载到那个位置。 wget https://dev.mysql.com/…...

2023 年会是网络安全的关键年吗?

过去 12 个月对网络安全领域和周围的每个人来说再次充满挑战。和往年不同&#xff0c;感觉很不一样&#xff0c;攻击源源不断。过去&#xff0c;大型漏洞每季度发生一次&#xff0c;但在过去一年中&#xff0c;在某些情况下&#xff0c;我们几乎每周都会处理严重漏洞。 已知利…...

【深度强化学习】(1) DQN 模型解析,附Pytorch完整代码

大家好&#xff0c;今天和各位讲解一下深度强化学习中的基础模型 DQN&#xff0c;配合 OpenAI 的 gym 环境&#xff0c;训练模型完成一个小游戏&#xff0c;完整代码可以从我的 GitHub 中获得&#xff1a; https://github.com/LiSir-HIT/Reinforcement-Learning/tree/main/Mod…...

Nginx服务优化与防盗链

目录 1.隐藏nginx版本号 1.查看版本号 2.隐藏版本信息 2.修改用户与组 3.缓存时间 4.日志分割 5.连接超时 6.更改进程数 7.网页压缩 8.配置防盗链 1.配置web源主机&#xff08;192.168.156.10 www.lhf.com&#xff09; 2.配置域名映射关系 3.配置盗链主机 &#xff0…...

npm与yarn常用命令

npm npm -v&#xff1a;查看 npm 版本npm init&#xff1a;初始化后会出现一个 Package.json 配置文件&#xff0c;可以在后面加上 -y&#xff0c;快速跳到问答界面npm install&#xff1a;会根据项目中的 package.json 文件自动给下载项目中所需的全部依赖npm insall 包含 --…...

【C++】C++11新特性——右值引用

文章目录一、左值引用、 右值引用1.1 左值与右值1.2 左值引用1.3 右值引用二、右值引用的意义三、移动语句3.1 移动构造3.2 移动赋值3.3 总结四、move问题五、完美转发5.1 万能引用与折叠5.2 完美转发std::forward一、左值引用、 右值引用 1.1 左值与右值 我们经常能听到左值…...

C#基础教程21 正则表达式

文章目录 简介正则表达式语法字符集元字符转义字符量词贪婪匹配和非贪婪匹配正则表达式类Regex类Match方法Matches方法简介 正则表达式是一种描述字符串模式的语言,它可以用来匹配、查找、替换字符串中的模式。在C#中,我们可以使用System.Text.RegularExpressions命名空间下的…...

聚观早报|谷歌发布最大视觉语言模型;王兴投资王慧文ChatGPT项目

今日要闻&#xff1a;谷歌发布全球最大视觉语言模型&#xff1b;马斯克预计Twitter下季度现金流转正&#xff1b;王兴投资王慧文ChatGPT项目&#xff1b;美国拟明年 11 月开展载人绕月飞行&#xff1b;慧与科技宣布收购Athonet谷歌发布全球最大视觉语言模型 近日&#xff0c;来…...

java Spring5 xml配置文件方式实现声明式事务

在java Spring5通过声明式事务(注解方式)完成一个简单的事务操作中 我们通过注解方式完成了一个事务操作 那么 下面 我还是讲一下 基于xml实现声明式事务的操作 其实在开发过程中 大家肯定都喜欢用注解 因为他方便 这篇文章中的xml方式 大家做个了解就好 还是 我们的这张表 记…...

Lombok 的 @Data 注解失效,未生成 getter/setter 方法引发的HTTP 406 错误

HTTP 状态码 406 (Not Acceptable) 和 500 (Internal Server Error) 是两类完全不同的错误&#xff0c;它们的含义、原因和解决方法都有显著区别。以下是详细对比&#xff1a; 1. HTTP 406 (Not Acceptable) 含义&#xff1a; 客户端请求的内容类型与服务器支持的内容类型不匹…...

基于ASP.NET+ SQL Server实现(Web)医院信息管理系统

医院信息管理系统 1. 课程设计内容 在 visual studio 2017 平台上&#xff0c;开发一个“医院信息管理系统”Web 程序。 2. 课程设计目的 综合运用 c#.net 知识&#xff0c;在 vs 2017 平台上&#xff0c;进行 ASP.NET 应用程序和简易网站的开发&#xff1b;初步熟悉开发一…...

FastAPI 教程:从入门到实践

FastAPI 是一个现代、快速&#xff08;高性能&#xff09;的 Web 框架&#xff0c;用于构建 API&#xff0c;支持 Python 3.6。它基于标准 Python 类型提示&#xff0c;易于学习且功能强大。以下是一个完整的 FastAPI 入门教程&#xff0c;涵盖从环境搭建到创建并运行一个简单的…...

工程地质软件市场:发展现状、趋势与策略建议

一、引言 在工程建设领域&#xff0c;准确把握地质条件是确保项目顺利推进和安全运营的关键。工程地质软件作为处理、分析、模拟和展示工程地质数据的重要工具&#xff0c;正发挥着日益重要的作用。它凭借强大的数据处理能力、三维建模功能、空间分析工具和可视化展示手段&…...

ffmpeg(四):滤镜命令

FFmpeg 的滤镜命令是用于音视频处理中的强大工具&#xff0c;可以完成剪裁、缩放、加水印、调色、合成、旋转、模糊、叠加字幕等复杂的操作。其核心语法格式一般如下&#xff1a; ffmpeg -i input.mp4 -vf "滤镜参数" output.mp4或者带音频滤镜&#xff1a; ffmpeg…...

什么是Ansible Jinja2

理解 Ansible Jinja2 模板 Ansible 是一款功能强大的开源自动化工具&#xff0c;可让您无缝地管理和配置系统。Ansible 的一大亮点是它使用 Jinja2 模板&#xff0c;允许您根据变量数据动态生成文件、配置设置和脚本。本文将向您介绍 Ansible 中的 Jinja2 模板&#xff0c;并通…...

【MATLAB代码】基于最大相关熵准则(MCC)的三维鲁棒卡尔曼滤波算法(MCC-KF),附源代码|订阅专栏后可直接查看

文章所述的代码实现了基于最大相关熵准则(MCC)的三维鲁棒卡尔曼滤波算法(MCC-KF),针对传感器观测数据中存在的脉冲型异常噪声问题,通过非线性加权机制提升滤波器的抗干扰能力。代码通过对比传统KF与MCC-KF在含异常值场景下的表现,验证了后者在状态估计鲁棒性方面的显著优…...

Qt 事件处理中 return 的深入解析

Qt 事件处理中 return 的深入解析 在 Qt 事件处理中&#xff0c;return 语句的使用是另一个关键概念&#xff0c;它与 event->accept()/event->ignore() 密切相关但作用不同。让我们详细分析一下它们之间的关系和工作原理。 核心区别&#xff1a;不同层级的事件处理 方…...

抽象类和接口(全)

一、抽象类 1.概念&#xff1a;如果⼀个类中没有包含⾜够的信息来描绘⼀个具体的对象&#xff0c;这样的类就是抽象类。 像是没有实际⼯作的⽅法,我们可以把它设计成⼀个抽象⽅法&#xff0c;包含抽象⽅法的类我们称为抽象类。 2.语法 在Java中&#xff0c;⼀个类如果被 abs…...

从实验室到产业:IndexTTS 在六大核心场景的落地实践

一、内容创作&#xff1a;重构数字内容生产范式 在短视频创作领域&#xff0c;IndexTTS 的语音克隆技术彻底改变了配音流程。B 站 UP 主通过 5 秒参考音频即可克隆出郭老师音色&#xff0c;生成的 “各位吴彦祖们大家好” 语音相似度达 97%&#xff0c;单条视频播放量突破百万…...