【JavaEE精炼宝库】多线程(5)单例模式 | 指令重排序 | 阻塞队列
目录
一、单例模式:
1.1 饿汉模式:
1.2 懒汉模式:
1.2.1 线程安全的懒汉模式:
1.2.2 线程安全的懒汉模式的优化:
二、指令重排序
三、阻塞队列
3.1 阻塞队列的概念:
3.2 生产者消费者模型:
3.3 标准库中的阻塞队列:
3.4 阻塞队列实现:
一、单例模式:
单例模式是校招中最常考的设计模式之一。
设计模式是什么?
设计模式好比象棋中的 "棋谱"。红方当头炮,黑方马来跳。针对红方的⼀些走法,黑方应招的时候有一些固定的套路。按照套路来走局势就不会吃亏。软件开发中也有很多常见的 "问题场景" 针对这些问题场景,大佬们总结出了一些固定的套路。按照这个套路来实现代码,也不会吃亏。大佬们为我们操碎了心。
单例模式能保证某个类在程序中只存在唯⼀⼀份实例,而不会创建出多个实例。这一点在很多场景上都需要。比如 JDBC 中的 DataSource 实例就只需要一个。
单例模式具体的实现方式有很多。最常见的是 "饿汉" 和 "懒汉" 两种。
1.1 饿汉模式:
类加载的同时,创建实例。
• 案例代码实现:
核心思想就是把构造方法设置为 private ,再把实例用 static 修饰。程序一运行,实例就被创建了,Singleton 类外面想要得到这个对象,只能通过 getInstance 来得到,所以能保证这个实例只被创建一次。
class Singleton{private static Singleton instance = new Singleton();//static 要记得加private Singleton(){}//这里要设置成 private,防止创建出多个实例public static Singleton getInstance(){return instance;}
}
1.2 懒汉模式:
类加载的时候不创建实例。第一次使用的时候才创建实例。
在计算机中 “懒” 是指高效的意思。这样如果后续这个类没有使用到,就可以把创建这个实例的损耗节省下来。
• 案例代码实现:
class SingletonLaze{private static SingletonLaze instance = null;private SingletonLaze(){}public static SingletonLaze getInstance(){if(instance == null){instance = new SingletonLaze();}return instance;}
}
到这里饿汉模式和懒汉模式的代码就已经大体编写完毕了。
请友友们思考一个问题:在多线程的情况下,上面的两种模式会出现线程不安全的情况嘛?
答:饿汉模式是线程安全的,懒汉模式是线程不安全的。
线程安全问题发生在首次创建实例时。如果在多个线程中同时调用 getInstance 方法,就可能导致创建出多个实例(虽然后续会被回收成一个,但是多个案例是实实在在被创建出来了,如果一个案例要使用 100G内存 ,会导致系统卡死的)。至于饿汉模式,在类加载的时候实例就已经被创建了,自然不存在线程安全问题。
1.2.1 线程安全的懒汉模式:
怎么解决懒汉模式的线程安全问题呢?
答:加锁。
加上 synchronized 可以改善这里的线程安全问题。
改进的案例代码如下:
class SingletonLaze {private static SingletonLaze instance = null;private static Object locker = new Object();private SingletonLaze() {}public static SingletonLaze getInstance() {synchronized (locker) {if(instance == null){//2 锁能不能加在 if 的里面instance = new SingletonLaze();}return instance;}}
}
这里友友们思考一下:锁能不能加在代码 2 的地方?
答:不能,如果多个线程同时进入的话,都能进入 if ,创建实例,那么锁就白加了。
到这里面试官可能还会问你,还能不能优化一下呢?
1.2.2 线程安全的懒汉模式的优化:
这里我们发现,只有在刚开始创建第一个实例的时候存在线程不安全的问题,创建完后,就和饿汉模式一样不会存在线程安全问题,这时代码还是一直加锁的话,会影响程序的效率,因为锁本身就是一个重量级操作。因此我们要在加锁的基础上,进一步改动。
• 使用双重 if 判定,降低锁竞争的频率。
• 给 instance 加上了 volatile。避免出现内存可见性导致的问题(这里概率很小)和指令重排序问题(大头)。
最终的代码如下:
class SingletonLaze {private static volatile SingletonLaze instance = null;private static Object locker = new Object();private SingletonLaze() {}public static SingletonLaze getInstance() {if (instance == null) {synchronized (locker) {if (instance == null) {instance = new SingletonLaze();}}}return instance;}
}
在多线程中,许多在单线程看起来毫无意义的操作,在多线程就可能有不同的作用,只是代码的编写恰好相同而已。第一个 if 是为了判断要不要加锁,第二个 if 是为了判断要不要创建对象。
二、指令重排序
指令重排序也是编译器的一种优化策略。
我们写的代码最终编译成了一系列的二进制指令。正常来说,CPU 是按照顺序,一条一条执行的。但是编译器比较智能,会根据实际情况,生成的二进制指令的执行顺序和我们最初写的代码顺序可能会存在差别,调整顺序最主要的目的就是提高效率。(前提要保证逻辑是等价的)
就好比如:田忌赛马。不同的执行顺序,产生的结果是截然不同的。
单线程下编译器的指令重排序一般都是没有问题的,但是在多线程的情况下,编译器的判定就可能不是那么的准确了。
在懒汉模式的优化那里如果不加上 volatile 关键字(防止指令重排序)可能会发生什么事情呢?
答:
instance = new SingletonLaze();
这一行代码,大体上可以分为如下三个步骤:
1. 申请内存空间。
2. 调用构造方法。(对内存空间进行初始化)
3. 将此时内存空间的地址,赋值给 instance 引用。
在指令重排序的优化策略下,上述执行的过程可能是1,2,3。也可能是1,3,2。如果是1,3,2的话,在多线程的情况下,可能就会有 bug 。在多线程的情况下,如果在第一个抢到锁的线程,创建实例,执行完1,3 再到 2 的这个过程中,如果有新的线程进来,那么在最外层 if 判断时,就会认为 instance 已经有实例了,直接返回一个空引用,这时正好这个引用被进行 ' . ' 操作,就会出现 bug。
上述谈到的指令重排序涉及到的 bug 是很难重现的,本身就是一个小概率事件。最好还是加上,如果出现问题,可能会带走年终奖😭。
三、阻塞队列
3.1 阻塞队列的概念:
队列我们已经很熟悉了。普通队列和优先级队列是线程不安全的。阻塞队列是一种特殊的队列。也遵守 "先进先出" 的原则。阻塞队列是一种线程安全的数据结构,并且具有以下特性:
• 当队列满的时候,继续入队列就会阻塞,直到有其他线程从队列中取走元素。
• 当队列空的时候,继续出队列也会阻塞,直到有其他线程往队列中插入元素。
阻塞队列的一个典型应用场景就是 "生产者消费者模型"。这是一种非常典型的开发模型。
3.2 生产者消费者模型:
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。
生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者,生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取。
生产者消费者模型,在开发中主要有两方面的意义:
• 阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。(削峰填谷)
• 阻塞队列也能使生产者和消费者之间解耦。
3.3 标准库中的阻塞队列:
在 Java 标准库中内置了阻塞队列。如果我们需要在一些程序中使用阻塞队列,直接使用标准库中的即可。
• BlockingQueue:接口
• ArrayBlockingQueue类:数组。
• LinkedBlockingQueue类 :链表。
• PriorityBlockingQueue类:堆 。
可以看到下面的三个类都实现了 BlockingQueue 接口。阻塞队列的使用方法如下:
• put 方法用于阻塞队列的入队列,take 用于阻塞队列的出队列(put、take 带有阻塞功能)。
• BlockingQueue 也有 offer, poll, peek 等方法,但是这些方法不带有阻塞特性。
案例演示:
import java.util.concurrent.*;
public class demo1 {public static void main(String[] args) {BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(100);//设置阻塞队列的容量为 100Thread producer = new Thread(() -> {//生产者for (int i = 1; i < 100000; i++) {try {System.out.println("生产:" + i);queue.put(i);} catch (InterruptedException e) {throw new RuntimeException(e);}}},"生产者");producer.start();Thread customer = new Thread(() -> {//消费者for (int i = 1; i < 100000; i++) {try {Thread.sleep(1000);int tmp = queue.take();System.out.println("消费:" + tmp);} catch (InterruptedException e) {throw new RuntimeException(e);}}},"消费者");customer.start();}
}
案例效果如下:
可以看到由于消费者被 sleep 了 1 秒,所以生产者马上就生产到了 100,到了 100 后由于阻塞队列具有阻塞功能,所以后续程序只能消费一个生产一个。
3.4 阻塞队列实现:
使用 synchronized 进行加锁控制。
要实现的功能有:
• put 插入元素的时候,判定如果队列满了,就进行 wait。(注意,要在循环中进行 wait。被唤醒时不一定队列就不满了,因为同时可能是唤醒了多个线程)。
• take 取出元素的时候,判定如果队列为空,就进行 wait 。(也是循环 wait)
具体的代码实现如下:
参数都在代码里面已经标好了,这里就不再赘述。唯一注意点就是在 wait 的条件语句使用 while 而不是 if。
public class MyBlockingQueue {private String[] elems = null;private volatile int tail = 0;//尾指针private volatile int head = 0;//头指针private volatile int size = 0;//大小public MyBlockingQueue(int capacity) {elems = new String[capacity];}/*** 把元素 elem 加入到队列中** @param elem*/public void put(String elem) throws InterruptedException {synchronized (this) {//保证线程安全while (size >= elems.length) {//最好写成 while//队列满的情况,阻塞this.wait();}//普通的队列操作elems[tail] = elem;size++;tail++;if (tail >= elems.length) {tail = 0;}this.notify();//唤醒 take }}/*** 从队列中取出 elem 元素* @return* @throws InterruptedException*///takepublic String take() throws InterruptedException {synchronized (this) {while (size == 0) {//当队列为空时,阻塞this.wait();}String result = elems[head];size--;head++;if (head >= elems.length) {head = 0;}this.notify();return result;//唤醒 put }}
}
演示效果:
可以看到和上面使用标准库中的阻塞队列功能基本一致。
结语:
其实写博客不仅仅是为了教大家,同时这也有利于我巩固知识点,和做一个学习的总结,由于作者水平有限,对文章有任何问题还请指出,非常感谢。如果大家有所收获的话还请不要吝啬你们的点赞收藏和关注,这可以激励我写出更加优秀的文章。
相关文章:

【JavaEE精炼宝库】多线程(5)单例模式 | 指令重排序 | 阻塞队列
目录 一、单例模式: 1.1 饿汉模式: 1.2 懒汉模式: 1.2.1 线程安全的懒汉模式: 1.2.2 线程安全的懒汉模式的优化: 二、指令重排序 三、阻塞队列 3.1 阻塞队列的概念: 3.2 生产者消费者模型…...

[图解]《分析模式》漫谈03-Party是什么
1 00:00:00,790 --> 00:00:03,930 今天我们来看一下,Party是什么 2 00:00:05,710 --> 00:00:07,470 当然我们这里说的不是政治的 3 00:00:07,880 --> 00:00:08,350 Party 4 00:00:09,230 --> 00:00:11,110 是《分析模式》里面的一个用词 5 00:00:14…...

【Numpy】一文向您详细介绍 np.abs()
【Numpy】一文向您详细介绍 np.abs() 下滑即可查看博客内容 🌈 欢迎莅临我的个人主页 👈这里是我静心耕耘深度学习领域、真诚分享知识与智慧的小天地!🎇 🎓 博主简介:985高校的普通本硕,曾…...

【AI绘画】Stable Diffusion 3开源
Open Release of Stable Diffusion 3 Medium 主要内容 Stable Diffusion 3是Stability AI目前为止最先进的文本转图像开放源代码算法。 这款模型的小巧设计使其完美适合用于消费级PC和笔记本电脑,以及企业级图形处理单元上运行。它已经满足了标准化的文字转图像模…...

使用ant-design/cssinjs向plasmo浏览器插件的内容脚本content中注入antd的ui组件样式
之前写过一篇文章用来向content内容脚本注入antd的ui:https://xiaoshen.blog.csdn.net/article/details/136418199,但是方法就是比较繁琐,需要将antd的样式拷贝出来,然后贴到一个单独的css样式文件中,然后引入到内容脚…...

南京威雅学校:初中转轨国际化教育,她们打开了成长的另一种可能
“上了大学就轻松了。” 又是一年高考季,每每回想起十八岁前那些没日没夜埋头学习的日子,已经为人父母的你是不是也忍不住想要孩子气地吐槽一句,“骗人”——人不会在一场考试后瞬间长大,试卷里也没有人生的全部答案。 三年前&a…...
Linux | 标准IO编程
Linux | 标准IO编程 时间:2024年6月8日23:03:43 文章目录 `Linux` | 标准`IO`编程1.标准`IO`编程1-1.流的打开函数fopen()1-2.流的关闭函数fclose()1-3.错误处理函数perror()函数strerror()errno 变量总结1-4.流的读写1-4-1.按字符(字节)输入/输出实例1-4-2.按行输入/输出1-…...

从ES的JVM配置起步思考JVM常见参数优化
目录 一、真实查看参数 (一)-XX:PrintCommandLineFlags (二)-XX:PrintFlagsFinal 二、堆空间的配置 (一)默认配置 (二)配置Elasticsearch堆内存时,将初始大小设置为…...

milvus的GPU索引
前言 milvus支持多种GPU索引类型,它能加速查询的性能和效率,特别是在高吞吐量,低延迟和高召回率的场景。本文我们将介绍milvus支持的各种GPU索引类型以及它们适用的场景、性能特点。 下图展示了milvus的几种索引的查询性能对比,…...

CleanMyMac2024最新免费电脑Mac系统优化工具
大家好,我是你们的好朋友——软件评测专家,同时也是一名技术博主。今天我要给大家种草一个超级实用的Mac优化工具——CleanMyMac! 作为一个长期使用macOS的用户,我深知系统运行时间长了,缓存文件、日志、临时文件等都会…...

catia/delmia的快捷图标模式最多12个
这儿最多显示12个 根据官方文档 If you installed a configuration containing more than 12 workbenches (such as the "AL2" configuration), only the first 12 workbenches are displayed in the Favorites list. The other workbenches do not appear in the l…...
磁盘性能概述与磁盘调度算法
目录 1. 磁盘性能概述 1. 数据传输速率 2. 寻道时间 3. 旋转延迟 4. 平均访问时间 2. 早期的磁盘调度算法 1. FIFO(First-In-First-Out)调度算法 2. SSTF(Shortest Seek Time First)调度算法 3. SCAN(Elevator…...

chrome浏览器设置--disable-web-security解决跨域
在开发人员于后台进行接口测试的时候,老是遇到跨域问题,这时前端总是会让后台添加跨域请求头来允许跨域请求,今天介绍一个简单的方法跨过这一步操作的设置。 –disable-web-security参数,禁用同源策略,利于开发人员本…...
Android中蓝牙设备的状态值管理
在Android中,蓝牙状态可以通过多种方式来描述,主要包括蓝牙适配器状态、蓝牙设备连接状态以及蓝牙广播状态,其关键的蓝牙状态实现类有BluetoothAdapter、BluetoothDevicePairer、BluetoothDevice、BluetoothProfile,详细介绍如下&…...
关于ReactV18的页面跳转传参和接收
一、使用路由方式进行传参和接收(此处需使用 useNavigate 和 useParams 两个hooks) 1 首先需要配置好路由形式如下 :id(参数) { path: "/articleDetail/:id", element: lazyElement(<ArticleDetail />), }, 2 传递参数 使用 useNaviga…...

南京观海微电子-----PCB设计怎样降低EMI
开关模式电源是AC-DC或DC-DC电源的通用术语,这些电源使用具有快速开关动作的电路进行电压转换/转换(降压或升压)。随着每天开发出更多的设备(潜在的EMI受害者),克服EMI成为工程师面临的主要挑战,并且实现电磁兼容性(EMC)与使设备正常运行同等…...
黑苹果/Mac如何升级 Mac 新系统 Sequoia Beta 版
Mac升级教程 有必要提醒一下大家,开发者测试版系统一般是给开发者测试用的,可能存在功能不完善、部分软件不兼容的情况,所以不建议普通用户升级,如果实在忍不住,升级之前记得做好备份。 升级方法很简单: …...
2024年主流工单系统横向对比
一:智齿科技 智齿客服App可以接收工单、查看工单、分配工单、处理工单,客户问题随时随地快速解决。 与云客户中心实时连接,客户以往的浏览轨迹、聊天信息、通话记录、工单历史一目了然。 配合智齿云呼叫中心/机器人客服/人工在线客服&…...

实用软件下载:Studio One最新安装包及详细安装教程
Studio One 6是一款功能丰富、专业级的音乐制作软件,它具备灵活的工作流程和高效的团队协作能力,能帮助用户实现高质量的音乐创作和制作。 智能模板更快的启动,全新的智能模板为你手头的任务提供了必要的工具集,包括基本录制、混音…...
网络安全练气篇——常见服务端口对应漏洞
常见的端口所对应的已知漏洞 21 FTP服务的数据传输端口 22 FTP服务的连接端口,可能存在 弱口令暴力破解 389 LDAP目录访问协议,有可能存在注入、弱口令 443 HTTPS端口,心脏滴血等与SSL有关的漏洞 445 SMB服务端口,可能存…...
【SpringBoot】100、SpringBoot中使用自定义注解+AOP实现参数自动解密
在实际项目中,用户注册、登录、修改密码等操作,都涉及到参数传输安全问题。所以我们需要在前端对账户、密码等敏感信息加密传输,在后端接收到数据后能自动解密。 1、引入依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId...

CMake基础:构建流程详解
目录 1.CMake构建过程的基本流程 2.CMake构建的具体步骤 2.1.创建构建目录 2.2.使用 CMake 生成构建文件 2.3.编译和构建 2.4.清理构建文件 2.5.重新配置和构建 3.跨平台构建示例 4.工具链与交叉编译 5.CMake构建后的项目结构解析 5.1.CMake构建后的目录结构 5.2.构…...
Leetcode 3577. Count the Number of Computer Unlocking Permutations
Leetcode 3577. Count the Number of Computer Unlocking Permutations 1. 解题思路2. 代码实现 题目链接:3577. Count the Number of Computer Unlocking Permutations 1. 解题思路 这一题其实就是一个脑筋急转弯,要想要能够将所有的电脑解锁&#x…...
Linux简单的操作
ls ls 查看当前目录 ll 查看详细内容 ls -a 查看所有的内容 ls --help 查看方法文档 pwd pwd 查看当前路径 cd cd 转路径 cd .. 转上一级路径 cd 名 转换路径 …...

dedecms 织梦自定义表单留言增加ajax验证码功能
增加ajax功能模块,用户不点击提交按钮,只要输入框失去焦点,就会提前提示验证码是否正确。 一,模板上增加验证码 <input name"vdcode"id"vdcode" placeholder"请输入验证码" type"text&quo…...
工程地质软件市场:发展现状、趋势与策略建议
一、引言 在工程建设领域,准确把握地质条件是确保项目顺利推进和安全运营的关键。工程地质软件作为处理、分析、模拟和展示工程地质数据的重要工具,正发挥着日益重要的作用。它凭借强大的数据处理能力、三维建模功能、空间分析工具和可视化展示手段&…...
Robots.txt 文件
什么是robots.txt? robots.txt 是一个位于网站根目录下的文本文件(如:https://example.com/robots.txt),它用于指导网络爬虫(如搜索引擎的蜘蛛程序)如何抓取该网站的内容。这个文件遵循 Robots…...

UR 协作机器人「三剑客」:精密轻量担当(UR7e)、全能协作主力(UR12e)、重型任务专家(UR15)
UR协作机器人正以其卓越性能在现代制造业自动化中扮演重要角色。UR7e、UR12e和UR15通过创新技术和精准设计满足了不同行业的多样化需求。其中,UR15以其速度、精度及人工智能准备能力成为自动化领域的重要突破。UR7e和UR12e则在负载规格和市场定位上不断优化…...
爬虫基础学习day2
# 爬虫设计领域 工商:企查查、天眼查短视频:抖音、快手、西瓜 ---> 飞瓜电商:京东、淘宝、聚美优品、亚马逊 ---> 分析店铺经营决策标题、排名航空:抓取所有航空公司价格 ---> 去哪儿自媒体:采集自媒体数据进…...

C++使用 new 来创建动态数组
问题: 不能使用变量定义数组大小 原因: 这是因为数组在内存中是连续存储的,编译器需要在编译阶段就确定数组的大小,以便正确地分配内存空间。如果允许使用变量来定义数组的大小,那么编译器就无法在编译时确定数组的大…...