【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 }}
}

演示效果:

可以看到和上面使用标准库中的阻塞队列功能基本一致。

结语:

其实写博客不仅仅是为了教大家,同时这也有利于我巩固知识点,和做一个学习的总结,由于作者水平有限,对文章有任何问题还请指出,非常感谢。如果大家有所收获的话还请不要吝啬你们的点赞收藏和关注,这可以激励我写出更加优秀的文章。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/26954.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Docker部署常见应用之大数据基础框架Hadoop

文章目录 Hadoop简介主要特点核心组件生态系统 Docker Compose 部署集群参考文章 Hadoop简介 Hadoop是一个开源框架&#xff0c;由Apache软件基金会开发&#xff0c;用于在普通硬件构建的集群中存储和处理大量数据。它最初由Doug Cutting和Mike Cafarella创建&#xff0c;并受…

H5小程序视频编辑解决方案,广泛适用,灵活部署

如何在微信小程序、网页、HTML5等WEB场景中实现轻量化视频制作&#xff0c;满足多样化的运营需求&#xff0c;一直是企业面临的挑战。美摄科技凭借其在视频编辑领域的深厚积累和创新技术&#xff0c;为企业量身打造了一套H5/小程序视频编辑解决方案&#xff0c;助力企业轻松应对…

C++笔记:模板

模板 为什么要学习模板编程 在学习模板之前&#xff0c;一定要有算法及数据结构的基础&#xff0c;以及重载&#xff0c;封装&#xff0c;多态&#xff0c;继承的基础知识&#xff0c;不然会出现看不懂&#xff0c;或者学会了没办法使用。 为什么C会有模板&#xff0c;来看下面…

JVM性能优化案例:减少对象频繁创建

JVM性能优化案例&#xff1a;减少对象频繁创建 案例背景 某金融应用系统在处理大量并发交易时&#xff0c;响应时间过长&#xff0c;并且有时出现内存溢出&#xff08;OutOfMemoryError&#xff09;的问题。经过分析&#xff0c;发现问题主要出在频繁的对象创建和较差的内存管…

git的ssh安装,windows通过rsa生成密钥认证问题解决

1 windows下载 官网下载可能出现下载太慢的情况&#xff0c;Git官网下载地址为&#xff1a;官网&#xff0c;推荐官网下载&#xff0c;如无法下载&#xff0c;可移步至CSDN&#xff0c;csdn下载地址&#xff1a;https://download.csdn.net/download/m0_46309087/12428308 2 Gi…

Perl 语言学习进阶

一、如何深入 要深入学习Perl语言的库和框架&#xff0c;可以按照以下步骤进行&#xff1a; 了解Perl的核心模块&#xff1a;Perl有许多核心模块&#xff0c;它们提供了许多常用的功能。了解这些模块的功能和用法是深入学习Perl的第一步。一些常用的核心模块包括&#xff1a;S…

如何在 Windows 10/11 上编辑 PDF [4 种简单方法]

PDF 在大多数设备上都易于查看&#xff0c;但由于其设计用于查看&#xff0c;因此编辑起来可能比较棘手。编辑 PDF 可能比编辑 Microsoft Office 文档更具挑战性。 不用担心&#xff0c;我们已经为你做好了准备。无论你是想添加、删除还是插入文本或图片&#xff0c;你都可以使…

Coze+Discord:打造你的免费AI助手(教您如何免费使用GPT-4o/Gemini等最新最强的大模型/Discord如何正确连接Coze)

文章目录 📖 介绍 📖🏡 演示环境 🏡📒 文章内容 📒📝 准备Discord📝 准备Coze🔌 连接💡 测试效果⚓️ 相关链接 ⚓️📖 介绍 📖 你是否想免费使用GPT-4o/Gemini等最新最强的大模型,但又不想花费高昂的费用?本文将教你如何通过Coze搭建Bot,并将其转发…

【AI绘画】Stable Diffusion 3开源

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

AI办公自动化:批量合并多个Excel表格的数据并汇总

工作任务&#xff1a; 有多个表格 把里面的月流量数据都合并到一张表中&#xff1a; 在chatgpt中输入提示词&#xff1a; 你是一个Python编程专家&#xff0c;要完成一个Python脚本编写任务&#xff0c;具体步骤如下&#xff1a; 打开文件夹&#xff1a;F:\AI自媒体内容\AI行…

计算机视觉全系列实战教程:(九)图像滤波操作

1.图像滤波的概述 (1)Why (为什么要进行图像滤波) 去噪&#xff1a;去除图像在获取、传输等过程中的各种噪音干扰提取特征&#xff1a;使用特定的图像滤波器提取图像特定特征 (2)What (什么是图像滤波) 使用滤波核对图像进行卷积运算或非线性运算&#xff0c;以达到去噪或提…

11.2 Go 常用包介绍

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:「stormsha的主页」…

使用‘消除’技术绕过LLM的安全机制,不用训练就可以创建自己的nsfw模型

开源的大模型在理解和遵循指令方面都表现十分出色。但是这些模型都有审查的机制&#xff0c;在获得被认为是有害的输入的时候会拒绝执行指令&#xff0c;例如会返回“As an AI assistant, I cannot help you.”。这个安全功能对于防止误用至关重要&#xff0c;但它限制了模型的…

化学品危险性分类鉴定报告 危化品危险性分类

一、化学品危险性分类报告&#xff1a; 按照国务院令 第591号 《危险化学品安全管理条例》、原十部委公告 2015年 第5号 《危险化学品目录&#xff08;2015版&#xff09;》、原安监总局令 第60号《化学品物理危险性鉴定与分类管理办法》和原安监总局令 第53号《危险化学品登记…

IBM Spectrum LSF Process Manager 在共享分布式计算环境中运行和管理业务关键工作流程

亮点 ● 快速创建复杂的分布式工作流 ● 开发可重复的最佳实践 ● 自信地运行关键工作流程 ● 提高流程可靠性 IBM Spectrum LSF Process Manager 使您能够设计和自动化计算或分析流程&#xff0c; 捕获和保护可重复的最佳实践。 使用直观的图形界面&#xff0c;您可以轻松记录…

【漏洞复现】飞企互联-FE企业运营管理平台 treeXml.jsp SQL注入漏洞

0x01 产品简介 飞企互联-FE企业运营管理平台是一个基于云计算、智能化、大数据、物联网、移动互联网等技术支撑的云工作台。这个平台可以连接人、链接端、联通内外&#xff0c;支持企业B2B、C2B与020等核心需求&#xff0c;为不同行业客户的互联网转型提供支持。其特色在于提供…

【十大排序算法】基数排序

数字犹如无数繁星&#xff0c;基数排序如晨曦的指引&#xff0c;将混沌序列织就成和谐的序曲。 文章目录 一、基数排序二、发展历史三、处理流程四、算法实现五、算法特性六、小结推荐阅读 一、基数排序 基数排序是一种非比较性的排序算法&#xff0c;它根据元素的位数来对元…

极限网关助力好未来 Elasticsearch 容器化升级

极限网关在好未来的最佳实践案例&#xff0c;轻松扛住日增百 TB 数据的流量&#xff0c;助力 ES 从物理机到云原生架构的改造&#xff0c;实现了流控、请求分析、安全管理、无缝迁移等场景。一次完美的客户体验~ 背景 物理机架构时代 2022 年&#xff0c;好未来整个日志 Elas…

教学辅助系统的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;教师管理&#xff0c;作业管理&#xff0c;学生管理&#xff0c;管理员管理&#xff0c;作业提交管理&#xff0c;教学视频管理 教室账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0…

React+TS前台项目实战(九)-- 全局常用组件弹窗Dialog封装

文章目录 前言Dialog公共弹窗组件1. 功能分析2. 代码详细注释3. 使用方式4. 效果展示 总结 前言 今天这篇主要讲全局公共弹窗Dialog组件封装&#xff0c;将用到上篇封装的模态框Modal组件。有时在前台项目中&#xff0c;偶尔要用到一两个常用的组件&#xff0c;如 弹窗&#x…