单例模式之懒汉式
在上篇文章中,我们讲了单例模式中的饿汉式,今天接着来讲懒汉式。
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的枚举类型—-枚举的线程安全性及序列化问题
相关文章:

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

1638_chdir函数的功能
全部学习汇总:GreyZhang/g_unix: some basic learning about unix operating system. (github.com) 今天看一个半生不熟的小函数,chdir。说半生不熟,是因为这个接口一看就知道是什么功能。然而,这个接口如何用可真就没啥想法了。 …...
使用CEF 获得某头条请求,并生成本地文件的方法
目录 一、获得网站请求响应信息 1、响应过滤 2、匹配过滤URL的函数 3、获得请求响应后的处理...
二十、Django-restframework之视图集和路由器
一、视图集和路由器 REST框架包含了一个处理视图集的抽象,它允许开发人员集中精力建模API的状态和交互,并根据通用约定自动处理URL构造。 视图集类与视图类几乎相同,不同之处在于它们提供的是retrieve或update等操作,而不是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++协程实现(时间轮定时器)
在后端的开发中,定时器有很广泛的应用。 比如: 心跳检测 倒计时 游戏开发的技能冷却 redis的键值的有效期等等,都会使用到定时器。 定时器的实现数据结构选择 红黑树 对于增删查,时间复杂度为O(logn),对于红黑…...
java多线程与线程池-04线程池与AQS
第7章 线程池与AQS java.util.concurrent包中的绝大多数同步工具,如锁(locks)和屏障(barriers)等,都基于AbstractQueuedSynchronizer(简称AQS)构建而成。这个框架提供了一套同步管理的通用机制,如同步状态的原子性管理、线程阻塞与解除阻塞,还有线程排队等。 在JD…...
优化模型验证关键代码25:样本均值近似技术处理两阶段随机旅行商问题及Gurobipy代码验证
大多数数学规划模型都会考虑到研究问题中存在的不确定性,针对这些不确定性,两种常用的处理方法是鲁棒优化和随机规划。这篇论文我们关注后者,也就是两阶段随机旅行商问题;利用套期保值算法计算不同规模TSP的可行解,同时比较了样本均值近似技术的解的情况,并计算了该问题的…...

老爸:“你做的什么游戏测试简直是不务正业!”——我上去就是一顿猛如虎的解释。
经常有人问我:游戏测试到底是干什么呢?是游戏代练?每天玩游戏?装备随便造,怪物随便秒,线上GM指令随便用?可以每天玩玩游戏,不用忙工作,太爽了?有时朋友不理解…...
JVM垃圾回收调优知识点整理
目录 1、JVM内存模型 1.2、堆及垃圾回收 1.3、JVM参数设置经验: 1.4、对象逃逸分析:...

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

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

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

Nginx服务优化与防盗链
目录 1.隐藏nginx版本号 1.查看版本号 2.隐藏版本信息 2.修改用户与组 3.缓存时间 4.日志分割 5.连接超时 6.更改进程数 7.网页压缩 8.配置防盗链 1.配置web源主机(192.168.156.10 www.lhf.com) 2.配置域名映射关系 3.配置盗链主机 ࿰…...
npm与yarn常用命令
npm npm -v:查看 npm 版本npm init:初始化后会出现一个 Package.json 配置文件,可以在后面加上 -y,快速跳到问答界面npm install:会根据项目中的 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项目
今日要闻:谷歌发布全球最大视觉语言模型;马斯克预计Twitter下季度现金流转正;王兴投资王慧文ChatGPT项目;美国拟明年 11 月开展载人绕月飞行;慧与科技宣布收购Athonet谷歌发布全球最大视觉语言模型 近日,来…...

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

19c补丁后oracle属主变化,导致不能识别磁盘组
补丁后服务器重启,数据库再次无法启动 ORA01017: invalid username/password; logon denied Oracle 19c 在打上 19.23 或以上补丁版本后,存在与用户组权限相关的问题。具体表现为,Oracle 实例的运行用户(oracle)和集…...

智慧医疗能源事业线深度画像分析(上)
引言 医疗行业作为现代社会的关键基础设施,其能源消耗与环境影响正日益受到关注。随着全球"双碳"目标的推进和可持续发展理念的深入,智慧医疗能源事业线应运而生,致力于通过创新技术与管理方案,重构医疗领域的能源使用模式。这一事业线融合了能源管理、可持续发…...

大话软工笔记—需求分析概述
需求分析,就是要对需求调研收集到的资料信息逐个地进行拆分、研究,从大量的不确定“需求”中确定出哪些需求最终要转换为确定的“功能需求”。 需求分析的作用非常重要,后续设计的依据主要来自于需求分析的成果,包括: 项目的目的…...

Redis相关知识总结(缓存雪崩,缓存穿透,缓存击穿,Redis实现分布式锁,如何保持数据库和缓存一致)
文章目录 1.什么是Redis?2.为什么要使用redis作为mysql的缓存?3.什么是缓存雪崩、缓存穿透、缓存击穿?3.1缓存雪崩3.1.1 大量缓存同时过期3.1.2 Redis宕机 3.2 缓存击穿3.3 缓存穿透3.4 总结 4. 数据库和缓存如何保持一致性5. Redis实现分布式…...

SCAU期末笔记 - 数据分析与数据挖掘题库解析
这门怎么题库答案不全啊日 来简单学一下子来 一、选择题(可多选) 将原始数据进行集成、变换、维度规约、数值规约是在以下哪个步骤的任务?(C) A. 频繁模式挖掘 B.分类和预测 C.数据预处理 D.数据流挖掘 A. 频繁模式挖掘:专注于发现数据中…...
Python爬虫实战:研究feedparser库相关技术
1. 引言 1.1 研究背景与意义 在当今信息爆炸的时代,互联网上存在着海量的信息资源。RSS(Really Simple Syndication)作为一种标准化的信息聚合技术,被广泛用于网站内容的发布和订阅。通过 RSS,用户可以方便地获取网站更新的内容,而无需频繁访问各个网站。 然而,互联网…...
OpenPrompt 和直接对提示词的嵌入向量进行训练有什么区别
OpenPrompt 和直接对提示词的嵌入向量进行训练有什么区别 直接训练提示词嵌入向量的核心区别 您提到的代码: prompt_embedding = initial_embedding.clone().requires_grad_(True) optimizer = torch.optim.Adam([prompt_embedding...
根据万维钢·精英日课6的内容,使用AI(2025)可以参考以下方法:
根据万维钢精英日课6的内容,使用AI(2025)可以参考以下方法: 四个洞见 模型已经比人聪明:以ChatGPT o3为代表的AI非常强大,能运用高级理论解释道理、引用最新学术论文,生成对顶尖科学家都有用的…...
Java + Spring Boot + Mybatis 实现批量插入
在 Java 中使用 Spring Boot 和 MyBatis 实现批量插入可以通过以下步骤完成。这里提供两种常用方法:使用 MyBatis 的 <foreach> 标签和批处理模式(ExecutorType.BATCH)。 方法一:使用 XML 的 <foreach> 标签ÿ…...

20个超级好用的 CSS 动画库
分享 20 个最佳 CSS 动画库。 它们中的大多数将生成纯 CSS 代码,而不需要任何外部库。 1.Animate.css 一个开箱即用型的跨浏览器动画库,可供你在项目中使用。 2.Magic Animations CSS3 一组简单的动画,可以包含在你的网页或应用项目中。 3.An…...