【并发专题】单例模式的线程安全(进阶理解篇)
目录
- 背景
- 前置知识
- 类加载运行全过程
- 单例模式的实现方式
- 一、饿汉式
- 基本介绍
- 源码
- 分析
- 二、懒汉式
- 基本介绍
- 源码
- 分析
- 改进
- 三、懒汉式单例终极解决方案(静态内部类)(推荐使用方案)
- 基本介绍
- 源码
- 分析
- 感谢
背景
最近学习了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.测试的分类有哪…...

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器的上位机配置操作说明
LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器专为工业环境精心打造,完美适配AGV和无人叉车。同时,集成以太网与语音合成技术,为各类高级系统(如MES、调度系统、库位管理、立库等)提供高效便捷的语音交互体验。 L…...
SkyWalking 10.2.0 SWCK 配置过程
SkyWalking 10.2.0 & SWCK 配置过程 skywalking oap-server & ui 使用Docker安装在K8S集群以外,K8S集群中的微服务使用initContainer按命名空间将skywalking-java-agent注入到业务容器中。 SWCK有整套的解决方案,全安装在K8S群集中。 具体可参…...

【kafka】Golang实现分布式Masscan任务调度系统
要求: 输出两个程序,一个命令行程序(命令行参数用flag)和一个服务端程序。 命令行程序支持通过命令行参数配置下发IP或IP段、端口、扫描带宽,然后将消息推送到kafka里面。 服务端程序: 从kafka消费者接收…...

2.Vue编写一个app
1.src中重要的组成 1.1main.ts // 引入createApp用于创建应用 import { createApp } from "vue"; // 引用App根组件 import App from ./App.vue;createApp(App).mount(#app)1.2 App.vue 其中要写三种标签 <template> <!--html--> </template>…...
Qt Http Server模块功能及架构
Qt Http Server 是 Qt 6.0 中引入的一个新模块,它提供了一个轻量级的 HTTP 服务器实现,主要用于构建基于 HTTP 的应用程序和服务。 功能介绍: 主要功能 HTTP服务器功能: 支持 HTTP/1.1 协议 简单的请求/响应处理模型 支持 GET…...
【C语言练习】080. 使用C语言实现简单的数据库操作
080. 使用C语言实现简单的数据库操作 080. 使用C语言实现简单的数据库操作使用原生APIODBC接口第三方库ORM框架文件模拟1. 安装SQLite2. 示例代码:使用SQLite创建数据库、表和插入数据3. 编译和运行4. 示例运行输出:5. 注意事项6. 总结080. 使用C语言实现简单的数据库操作 在…...

前端开发面试题总结-JavaScript篇(一)
文章目录 JavaScript高频问答一、作用域与闭包1.什么是闭包(Closure)?闭包有什么应用场景和潜在问题?2.解释 JavaScript 的作用域链(Scope Chain) 二、原型与继承3.原型链是什么?如何实现继承&a…...

用机器学习破解新能源领域的“弃风”难题
音乐发烧友深有体会,玩音乐的本质就是玩电网。火电声音偏暖,水电偏冷,风电偏空旷。至于太阳能发的电,则略显朦胧和单薄。 不知你是否有感觉,近两年家里的音响声音越来越冷,听起来越来越单薄? —…...

iview框架主题色的应用
1.下载 less要使用3.0.0以下的版本 npm install less2.7.3 npm install less-loader4.0.52./src/config/theme.js文件 module.exports {yellow: {theme-color: #FDCE04},blue: {theme-color: #547CE7} }在sass中使用theme配置的颜色主题,无需引入,直接可…...

《Docker》架构
文章目录 架构模式单机架构应用数据分离架构应用服务器集群架构读写分离/主从分离架构冷热分离架构垂直分库架构微服务架构容器编排架构什么是容器,docker,镜像,k8s 架构模式 单机架构 单机架构其实就是应用服务器和单机服务器都部署在同一…...