【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.rhkb.cn/news/351849.html

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

相关文章

GPU的工作原理

location: Beijing 1. why is GPU CPU的存储单元和计算单元的互通过慢直接促进了GPU的发展 先介绍一个概念&#xff1a;FLOPS&#xff08;Floating Point Operations Per Second&#xff0c;浮点运算每秒&#xff09;是一个衡量其执行浮点运算的能力&#xff0c;可以作为计算…

板凳----Linux/Unix 系统编程手册 25章 进程的终止

25.1 进程的终止&#xff1a;_exit()和exit() 440 1. _exit(int status)&#xff0c; status 定义了终止状态&#xff0c;父进程可调用 wait 获取。仅低8位可用&#xff0c;调用 _exit() 总是成功的。 2.程序一般不会调用 _exit()&#xff0c; 而是调用库函数 exit()。exit() …

python-不定方程求解

[题目描述] 给定正整数 a&#xff0c;b&#xff0c;c。求不定方程axbyc 关于未知数 x 和 y 的所有非负整数解组数。输入&#xff1a; 一行&#xff0c;包含三个正整数 a&#xff0c;b&#xff0c;c&#xff0c;两个整数之间用单个空格隔开。每个数均不大于 1000。输出&#xff…

游戏中插入音效

一、背景音乐 准备&#xff1a;素材音乐 方法&#xff1a; 1、方法1&#xff1a; (1) 将背景音乐 bgAudio 拖放到Hierarchy面板 (2) 选中 bgAudio&#xff0c;勾选开始运行就播放、循环播放。调节音量&#xff08;volume) 2、方法2&#xff1a; (1) Create Empty&#x…

【Android】安卓开发的前景

人不走空 &#x1f308;个人主页&#xff1a;人不走空 &#x1f496;系列专栏&#xff1a;算法专题 ⏰诗词歌赋&#xff1a;斯是陋室&#xff0c;惟吾德馨 目录 &#x1f308;个人主页&#xff1a;人不走空 &#x1f496;系列专栏&#xff1a;算法专题 ⏰诗词歌…

C# 设置PDF表单不可编辑、或提取PDF表单数据

PDF表单是PDF中的可编辑区域&#xff0c;允许用户填写指定信息。当表单填写完成后&#xff0c;有时候我们可能需要将其设置为不可编辑&#xff0c;以保护表单内容的完整性和可靠性。或者需要从PDF表单中提取数据以便后续处理或分析。 之前文章详细介绍过如何使用免费Spire.PDF…

【面试题】MySQL常见面试题总结

备战实习&#xff0c;会定期给大家整理常考的面试题&#xff0c;大家一起加油&#xff01; &#x1f3af; 系列文章目录 【面试题】面试题分享之JVM篇【面试题】面试题分享之Java并发篇【面试题】面试题分享之Java集合篇&#xff08;三&#xff09; 注意&#xff1a;文章若有错…

WPF视频学习-简单应用篇图书馆程序(一)

1.登录界面和主界面跳转 先把登录界面分为三行《Grid》 先添加两行&#xff1a; <Grid><!--//分三行&#xff0c;行排列--><Grid.RowDefinitions><RowDefinition Height"auto"/><RowDefinition Height"auto"/><RowDef…

m4s转mp3——B站缓存视频提取音频

前言 しかのこのこのここしたんたん&#xff08;鹿乃子乃子虎视眈眈&#xff09;非常之好&#xff0c;很适合当闹钟&#xff0c;于是缓存了视频&#xff0c;想提取音频为mp3 直接改后缀可乎&#xff1f;格式转换工具&#xff1f; 好久之前有记录过转MP4的&#xff1a; m4s转为…

Python自动化办公(一) —— 根据PDF文件批量创建Word文档

Python自动化办公&#xff08;一&#xff09; —— 根据PDF文件批量创建Word文档 在日常办公中&#xff0c;我们经常需要根据现有的PDF文件批量创建Word文档。手动操作不仅费时费力&#xff0c;而且容易出错。幸运的是&#xff0c;使用Python可以轻松实现这个过程。本文将介绍如…

python14 字典类型

字典类型 键值对方式&#xff0c;可变数据类型&#xff0c;所以有增删改功能 声明方式1 {} 大括号&#xff0c;示例 d {key1 : value1, key2 : value2, key3 : value3 ....} 声明方式2 使用内置函数 dict() 创建1)通过映射函数创建字典zip(list1,list2) 继承了序列的所有操作 …

基于GA遗传算法的多机无源定位系统GDOP优化matlab仿真

目录 1.程序功能描述 2.测试软件版本以及运行结果展示 3.核心程序 4.本算法原理 5.完整程序 1.程序功能描述 基于GA遗传算法的多机无源定位系统GDOP优化matlab仿真。仿真输出GDOP优化结果&#xff0c;遗传算法的优化收敛曲线以及三维空间坐标点。 2.测试软件版本以及运行…

关于HTTP劫持,该如何理解、防范和应对

一、引言 HTTP劫持&#xff08;HTTP Hijacking&#xff09;是一种网络安全威胁&#xff0c;它发生在HTTP通信过程中&#xff0c;攻击者试图通过拦截、篡改或监控用户与服务器之间的数据流量&#xff0c;以达到窃取敏感信息或执行恶意操作的目的。今天我们就来详细了解HTTP劫持…

gma 2.0.10 (2024.06.16) | GmaGIS V0.0.0a4 更新日志

安装 gma 2.0.10 pip install gma2.0.10网盘下载&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1P0nmZUPMJaPEmYgixoL2QQ?pwd1pc8 提取码&#xff1a;1pc8 注意&#xff1a;此版本没有Linux版&#xff01; 编译gma的Linux虚拟机没有时间修复&#xff0c;本期Linux版…

vue 之 vuex

目录 vuex 是什么 Vuex管理哪些状态呢&#xff1f; Vuex 页面刷新数据丢失怎么解决 1. 使用浏览器的本地存储 2. 使用 Vuex 持久化插件 3. 使用后端存储 注意事项 Vuex 为什么要分模块并且加命名空间 vuex 是什么 vuex 是专门为 vue 提供的全局状态管理系统&#xff0c…

物理隔离后数据怎么导入和导出?安全U盘一键解决

政府单位、军工和科研所、航空航天企业、金融机构、医疗单位、电力企业、生物制药实验室等企业及单位&#xff0c;因研发和生产过程、或日常经营中涉及大量敏感信息和技术&#xff0c;需要通过物理隔离来确保网络的安全性。因此&#xff0c;多采用物理隔离的方式进行网络建设。…

[c++刷题]贪心算法.N01

题目如上: 首先通过经验分析&#xff0c;要用最少的减半次数&#xff0c;使得数组总和减少至一半以上&#xff0c;那么第一反应就是每次都挑数组中最大的数据去减半&#xff0c;这样可以是每次数组总和值减少程度最大化。 代码思路:利用大根堆去找数据中的最大值&#xff0c;…

LeetCode | 520.检测大写字母

这道题直接分3种情况讨论&#xff1a;1、全部都为大写&#xff1b;2、全部都为小写&#xff1b;3、首字母大写其余小写。这里我借用了一个全是大写字母的串和一个全为小写字母的串进行比较 class Solution(object):def detectCapitalUse(self, word):""":type …

LeetCode347:前K个高频元素

题目描述 给你一个整数数组 nums 和一个整数 k &#xff0c;请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。 解题思想 使用优先队列 priority_queue<Type, Container, Functional> Type 就是数据类型&#xff0c;Container 就是容器类型&#xff08;C…

unity跑酷游戏(源码)

包括&#xff1a;触发机关&#xff0c; 优化 fog的调试 效果 碰到障碍物游戏时间暂停&#xff08;挂载到障碍物上&#xff09; 上面需要有碰撞体 游戏物体上需要有标签 using System.Collections; using System.Collections.Generic; using UnityEngine;public class Barri…