【并发专题】单例模式的线程安全(进阶理解篇)
目录
- 背景
- 前置知识
- 类加载运行全过程
- 单例模式的实现方式
- 一、饿汉式
- 基本介绍
- 源码
- 分析
- 二、懒汉式
- 基本介绍
- 源码
- 分析
- 改进
- 三、懒汉式单例终极解决方案(静态内部类)(推荐使用方案)
- 基本介绍
- 源码
- 分析
- 感谢
背景
最近学习了JVM之后,总感觉知识掌握不够深,所以想通过分析经典的【懒汉式单例】来加深一下理解。(主要是【静态内部类】实现单例的方式)。
如果小白想理解单例的话,也能看我这篇文章。我也通过了【前置知识】跟【普通懒汉式】、【双检锁懒汉】、【静态内部类】懒汉给大家分析了一下他们的线程安全性。但是,我这边没有完整的演进【懒汉式单例】历程。所以,会缺少思维上的递进。不过,我在最后的【感谢】名单里,提供了一个完整的【懒汉式单例演进】的链接,建议可以结合这个文章一起学习。
前置知识
类加载运行全过程
当我们用java命令运行某个类的main函数启动程序时,首先需要通过类加载器把主类加载到JVM。
package com.tuling.jvm;public class Math {public static final int initData = 666;public static User user = new User();public int compute() { //一个方法对应一块栈帧内存区域int a = 1;int b = 2;int c = (a + b) * 10;return c;}public static void main(String[] args) {Math math = new Math();math.compute();}
}
通过Java命令执行代码的大体流程如下:

其中loadClass的类加载过程有如下几步:
加载 >> 验证 >> 准备 >> 解析 >> 初始化 >> 使用 >> 卸载
- 加载:在硬盘上查找并通过IO读入字节码文件,使用到类时才会加载,例如调用类的main()方法,new对象等等,在加载阶段会在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
- 验证:校验字节码文件的正确性
- 准备:给类的静态变量分配内存,并赋予默认值
- 解析:将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如main()方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用,下节课会讲到动态链接
- 初始化:对类的静态变量初始化为指定的值,执行静态代码块

总结一下,上面说的加载 >> 验证 >> 准备 >> 解析 >> 初始化过程是由JVM帮我们进行的,所以,对我们程序员来说,【天生】就具备线程安全性(这个由JVM帮我们保证,无需我们关心)。
单例模式的实现方式
单例模式,是我们Java中很常见的一个设计模式。所以有这么一种说法:遇事不决,单例解决。
Java单例通常有2种,分别为:饿汉式、懒汉式
一、饿汉式
基本介绍
饿汉式(Eager Initialization,急切的初始化),在类加载时就创建单例实例,并在需要时直接返回该实例。这种方式的实现是线程安全的,因为在类加载过程中实例已经创建好了。
源码
public class SingletonTest {private static final SingletonTest me = new SingletonTest();public static SingletonTest me() {return me;}public static void main(String[] args) {System.out.println(SingletonTest.me());System.out.println(SingletonTest.me());System.out.println(SingletonTest.me());}
// 系统输出如下:
// org.tuling.juc.singleton.SingletonTest@12a3a380
// org.tuling.juc.singleton.SingletonTest@12a3a380
// org.tuling.juc.singleton.SingletonTest@12a3a380
}
分析
因为单例对象SingletonTest 是静态成员变量,所以,在JVM类加载过程中==(加载-》验证-》准备-》解析-》初始化)==的【解析】阶段已经被JVM初始化了,所以,由JVM保证了线程安全性。
二、懒汉式
基本介绍
懒汉式(Lazy Initialization),在首次调用时创建单例实例,存在线程安全问题。如果多个线程同时进入判断条件,可能会创建多个实例。
源码
public class SingletonTest {private static SingletonTest me;public static SingletonTest me() {if(me == null) {me = new SingletonTest();}return me;}public static void main(String[] args) {for (int i = 0; i < 10; i++) {new Thread(()->{System.out.println(SingletonTest.me());}).start();}}
}
输出结果如下

分析
为什么上面这段代码不是线程安全的呢?我们举一个极端的例子,如下图所示:

在没有锁机制的存在情况下,多线程环境里面可能会出现上述的并发执行情况。在线程1判断完me == null之后,即将开始执行new之前,线程2也刚好在判断me == null,这是因为线程1还没有执行new操作,所以线程2判断肯定是null的,于是也开始new。这就是线程安全问题所在。
(PS:小白们一定要理解上面这个图。虽然很简单,但是说它是你们迈向,或者培养【并发意识】的启蒙都不为过。)
改进
为了解决上面的问题,大牛们进行了改进,使用了【双检锁+volatile】机制,【双检锁】,即:双重检查锁。代码如下:
public class SingletonTest {private static volatile SingletonTest me;public static SingletonTest me() {if(me == null) {synchronized (SingletonTest.class) {if (me == null) {me = new SingletonTest();}}}return me;}
}
上面的改进,关键点如下:
- 使用了
volatile关键字修饰单例对象me - 在获取单例对象的时候,判断了两次
if(me == null) - 第二次判断
if(me == null)之前,先加了锁
第二、三点我就不说了,大家可以看看最下面【感谢】的友链。这里重点说说第一点。
估计小白会很难理解,为什么一定要volatile关键字修饰,不用可以吗?答案是:不可以。因为,volatile能禁止重排序。什么是【重排序呢】?说的简单点,就是JVM,甚至是CPU为了性能,可能会在不改变语义的情况下修改我们的代码执行顺序。比如,当我们new SingletonTest()的时候,你以为只有一步操作,实际上,它有3步,如下:
memory = allocate(); // 1.分配对象内存空间
instance(memory); // 2.初始化对象
instance = memory; // 3.设置instance指向刚分配的内存地址,此时instance!=null
但事实上,经过重排序之后可能会变成下面的执行顺序:
memory = allocate(); // 1.分配对象内存空间
instance = memory; // 3.设置instance指向刚分配的内存地址,此时instance!=null
instance(memory); // 2.初始化对象
然后大家再用上面的【并发启蒙】意识,自己画个图看下,还能线程安全吗?
所以,需要使用volatile关键字,告诉底层JVM或者CPU,不要帮我重排序这个对象!于是就避免了上面的并发线程安全问题了。
三、懒汉式单例终极解决方案(静态内部类)(推荐使用方案)
基本介绍
这里通过利用JVM类加载【天生线程安全】的特性,来帮助实现【懒汉式】的单例。如何做到呢?答案是【静态内部类】。
源码
public class SingletonTest {/** 单例对象,可以直接调用配置属性 */private static class Holder {private static SingletonTest me = new SingletonTest();}public static SingletonTest me() {return Holder.me;}public static void main(String[] args) {int threadCount = 10000;for (int i = 0; i < threadCount; i++) {new Thread(()->{System.out.println(SingletonTest.me());}).start();}}
}
上面的代码,新建了1W个线程来调用单例,我们发现,结果都是一样,同一个对象。

分析
为什么上面,通过静态内部类能保证线程安全性呢?这个我们在【前置知识】已经说过了,是由JVM保证了线程安全性。

如上图所示,只有当我们使用了SingletonTest.me()的时候,才会去开始加载Holder静态内部类,这就是它实现【懒汉式】的原因(延迟加载)。
感谢
感谢【作者:weixin_47196090】的深度好文,《懒汉式单例演进到DCL懒汉式 深度全面解析》
相关文章:
【并发专题】单例模式的线程安全(进阶理解篇)
目录 背景前置知识类加载运行全过程 单例模式的实现方式一、饿汉式基本介绍源码分析 二、懒汉式基本介绍源码分析改进 三、懒汉式单例终极解决方案(静态内部类)(推荐使用方案)基本介绍源码分析 感谢 背景 最近学习了JVM之后&…...
无涯教程-Perl - if...elsif...else语句函数
if 语句后可以跟可选的 elsif ... else 语句,这对于使用单个if ... elsif语句测试各种条件非常有用。 if...elsif...else - 语法 Perl编程语言中的 if ... elsif...else语句的语法是- if(boolean_expression 1) {# Executes when the boolean expression 1 is tr…...
uniapp 实现滑动元素并下方有滚动条显示
用uniapp实现下图的样式 代码如下: <template><view class"content"><view class"data-box" ref"dataBox" touchend"handleEnd"><view class"data-list"><view class"data-ite…...
QT充当客户端模拟浏览器等第三方客户端对https进行双向验证
在 ssl单向证书和双向证书校验测试及搭建流程 文章中,已经做了基于https的单向认证和双向认证,,, 在进行双向认证时,采用的是curl工具或浏览器充当客户端去验证。 此次采用QT提供的接口去开发客户端向服务器发送请求&a…...
【JVM】 垃圾回收篇——自问自答(1)
Q什么是垃圾: 运行程序中,没用任何指针指向的对象。 Q为什么需要垃圾回收? 内存只分配,不整理回收,迟早会被消耗完。 内存碎片的整理,为新对象腾出空间 没有GC程序无法正常进行。 Q 哪些区域有GC&#…...
Image Line FL Studio v21.0.3.3517 Producer版全插件版WIN免费下载完整版
FL Studio 21,也称为 Fruity Loops 21,是一款功能强大的数字音频工作站,被世界各地的音乐制作人和 DJ 使用。无论您是新手还是经验丰富的制作人,FL Studio 21都能为您提供创作专业品质音乐所需的工具。在这篇博文中,我…...
PHP8条件控制语句-PHP8知识详解
我们昨天说了流程控制的结构有顺序结构、选择结构和循环结构。选择结构就是条件结构。 条件控制语句就是对语句中不同条件的值进行判断,进而根据不同的条件执行不同的语句。 在本文中,学习的是if语句、if…else语句、if…elseif语句和switch语句。 1、…...
【PHP代码审计】ctfshow web入门 php特性 93-104
ctfshow web入门 php特性 93-104 web 93web 94web 95web 96web 97web 98web 99web 100web 101web 102web 103web 104 web 93 这段PHP代码是一个简单的源码审计例子,让我们逐步分析它: include("flag.php");: 这行代码将flag.php文件包含进来。…...
CSS元素的显示模式
1、现在我想做成小米左侧边栏这样的效果,该怎么做呢? 2、小米商城触碰之后会显示出新的商品案例 3、一碰到之后会出现这个列表 4、这里涉及到了元素显示模式: 5、用人进行划分可以分为男人和女人,根据男人和女人的特性进行相应的…...
Go strings.Title方法被废弃(Deprecated)
strings.Title的使用 在传统中,我们可以通过如下形式将每个单词的首字母变成大写字母,示例如下: func TestTitle(t *testing.T) { fmt.Println(strings.Title("hello world")) fmt.Println(strings.Title("hell golang&qu…...
vuejs源码分析之全局API(vm.$off)
vue在初始化的时候会给vue对象本身挂载一些全局的api。今天我们一个一个来看这些api。 vm.$off方法 这个方法是用来移除自定义事件监听器。 他的用法 vm.$off(event, calback)第一个参数event取值可以是string字符串,也可以是Array<string>也就是说既可以删…...
elasticSearch常见的面试题
常见的面试问题 描述使用场景 es集群架构3个节点,根据不同的服务创建不同的索引,根据日期和环境,平均每天递增60*2,大约60Gb的数据。 调优技巧 原文参考:干货 | BAT等一线大厂 Elasticsearch面试题解读 - 掘金 设计阶…...
第一课-前提-Stable Diffusion 教程
学习 SD 的前提是电脑配置! SD 参考配置: 建议选择台式机 i5 CPU, 内存16GB,N卡 RTX3060, 8G显存以上的配置(最低配) 在此基础上的配置越高越好。 比如,cpu i7 更好,显卡能有 RTX4090 更好,32显存要能有最好,嘿嘿嘿。 如何查看自己的显卡配置? Win+R 输入 “dxdiag…...
Python 开发工具 Pycharm —— 使用技巧Lv.2
pydoc是python自带的一个文档生成工具,使用pydoc可以很方便的查看类和方法结构 本文主要介绍:1.查看文档的方法、2.html文档说明、3.注释方法、 一、查看文档的方法 **方法1:**启动本地服务,在web上查看文档 命令【python3 -m…...
代码随想录第39天 | 62. 不同路径、63.不同路径II
62. 不同路径 动态规划五部曲: dp[i][j] :表示从(0 ,0)出发,到(i, j) 有dp[i][j]条不同的路径。想要求dp[i][j],只能有两个方向来推导出来,即dp[i - 1][j] 和 dp[i][j - 1]。dp[i]…...
QMT入门—初识QMT
对于普通投资者来说,每天实时盯盘实在是无聊又无趣,特别是临时有事还会错过行情。如果能把自己的投资策略用代码实现,通过程序来自动买卖股票那该有多好,这样就不会错过行情也不会不按交易纪律来操作了。 解决办法有两种…...
C 语言的 return 语句
有返回值的函数要带 return 语句, return 后面是一个表达式, return 语句将表达式的值返回给主调函数. 一个函数也可以有多个 return 语句, 比如存在于不同的分支中, 但只能有一条 return 语句被执行, 然后程序的控制权就从被调函数传到主调函数. 对于有返回值但没有带 retur…...
企业级Vue路由角色权限应该怎么做?
角色权限 角色权限,简单来说就是登录的用户能看到系统的哪些页面,不能看到系统的哪些页面。一般是后台管理系统才会涉及到如此复杂的角色权限。 对于 vue 技术栈,实现角色权限一般有两种方式。 第一种是利用 beforeEach 全局前置守卫。 第…...
3.2.0 版本预告!Apache DolphinScheduler API 增强相关功能
Apache DolphinScheduler 3.2.0 版本即将发布,在此之前,为了让用户提前了解到大家所期待的新功能,我们制作了视频来”剧透“一些核心新发布。此前,我们比较全面地”剧透“的 3.2.0 版本的新功能,这次,我们来…...
测试工程师的工作
目录 1.何为软件测试工程师? 2.软件测试工程师的职责? 3.为什么要做软件测试? 4.软件测试的前途如何? 5.工具和思维谁更重要? 6.测试和开发相差大吗? 7.成为测试工程师的必备条件 8.测试的分类有哪…...
IDEA运行Tomcat出现乱码问题解决汇总
最近正值期末周,有很多同学在写期末Java web作业时,运行tomcat出现乱码问题,经过多次解决与研究,我做了如下整理: 原因: IDEA本身编码与tomcat的编码与Windows编码不同导致,Windows 系统控制台…...
【Java学习笔记】Arrays类
Arrays 类 1. 导入包:import java.util.Arrays 2. 常用方法一览表 方法描述Arrays.toString()返回数组的字符串形式Arrays.sort()排序(自然排序和定制排序)Arrays.binarySearch()通过二分搜索法进行查找(前提:数组是…...
系统设计 --- MongoDB亿级数据查询优化策略
系统设计 --- MongoDB亿级数据查询分表策略 背景Solution --- 分表 背景 使用audit log实现Audi Trail功能 Audit Trail范围: 六个月数据量: 每秒5-7条audi log,共计7千万 – 1亿条数据需要实现全文检索按照时间倒序因为license问题,不能使用ELK只能使用…...
python报错No module named ‘tensorflow.keras‘
是由于不同版本的tensorflow下的keras所在的路径不同,结合所安装的tensorflow的目录结构修改from语句即可。 原语句: from tensorflow.keras.layers import Conv1D, MaxPooling1D, LSTM, Dense 修改后: from tensorflow.python.keras.lay…...
基于Springboot+Vue的办公管理系统
角色: 管理员、员工 技术: 后端: SpringBoot, Vue2, MySQL, Mybatis-Plus 前端: Vue2, Element-UI, Axios, Echarts, Vue-Router 核心功能: 该办公管理系统是一个综合性的企业内部管理平台,旨在提升企业运营效率和员工管理水…...
嵌入式常见 CPU 架构
架构类型架构厂商芯片厂商典型芯片特点与应用场景PICRISC (8/16 位)MicrochipMicrochipPIC16F877A、PIC18F4550简化指令集,单周期执行;低功耗、CIP 独立外设;用于家电、小电机控制、安防面板等嵌入式场景8051CISC (8 位)Intel(原始…...
React核心概念:State是什么?如何用useState管理组件自己的数据?
系列回顾: 在上一篇《React入门第一步》中,我们已经成功创建并运行了第一个React项目。我们学会了用Vite初始化项目,并修改了App.jsx组件,让页面显示出我们想要的文字。但是,那个页面是“死”的,它只是静态…...
二维数组 行列混淆区分 js
二维数组定义 行 row:是“横着的一整行” 列 column:是“竖着的一整列” 在 JavaScript 里访问二维数组 grid[i][j] 表示 第i行第j列的元素 let grid [[1, 2, 3], // 第0行[4, 5, 6], // 第1行[7, 8, 9] // 第2行 ];// grid[i][j] 表示 第i行第j列的…...
更新 Docker 容器中的某一个文件
🔄 如何更新 Docker 容器中的某一个文件 以下是几种在 Docker 中更新单个文件的常用方法,适用于不同场景。 ✅ 方法一:使用 docker cp 拷贝文件到容器中(最简单) 🧰 命令格式: docker cp <…...
uni-app学习笔记二十七--设置底部菜单TabBar的样式
官方文档地址:uni.setTabBarItem(OBJECT) | uni-app官网 uni.setTabBarItem(OBJECT) 动态设置 tabBar 某一项的内容,通常写在项目的App.vue的onLaunch方法中,用于项目启动时立即执行 重要参数: indexnumber是tabBar 的哪一项&…...
