[Java EE] 多线程(五):单例模式与阻塞队列

1. 单例模式

单例模式是校招中最长考的设计模式之一,首先我们来谈一谈什么是设计模式:

设计模式就好像象棋中的棋谱一样,如果红方走了什么样的局势,黑方就有一定地固定地套路,来应对这样的局势,按照固定地套路来,可以保证在该局势下不会吃亏.
在这里插入图片描述
软件开发也是同样的道理,有很多的问题场景,针对这些问题场景,一些编程界的大佬就总结出了一套固定的解决模版,只要按着这个套路来,就会避免出现各种各样的bug.

软件设计中的单例模式就是保证每个程序运行的过程中只存在一份实例,而不会创建多个实例.这种设计模式在我们的日常开发中比比皆是,使用单例模式也可以大大提高程序的效率,下面我们来举个例子:
假如服务器中药从硬盘上读取100G的固定数据到内存中,这样的操作肯定是通过一个类来封装,这样的类最好是单例的,因为数据一样,在创建多个实例的时候,就会对系统资源开销很大,而且多次读取同样数据这样的操作也没必要,让他们共享同一份数据就可以了.

但是如何保证每个类只创建一个实例出来呢?这可不是程序员口头保证就行,只要是人去做,都不靠谱,我们应该把这样的操作交给计算机来处理,需要让编译器来帮我们做一个强制的检查.

通常的单例模式有以下两种,懒汉模式和饿汉模式:

1.1 饿汉模式

所谓饿汉模式,就突出一个字:,就好像一个饿汉被饿了好几天想要得到食物一样.我们需要在JVM启动的时候,即类刚刚加载的时候,就创建这个类的实例.其次,我们为了该类只创建一个实例,防止通过构造方法来创建多余的实例,所以我们把构造方法设置为私有.之后要想获取这个类的实例,我们可以通过一个普通方法来获取类中创建的这个实例.代码如下:

/*** 饿汉模式*/
class Singleton{public static Singleton instance = new Singleton();//在加载类的时候就创建实例private Singleton(){}//通过把构造方法设计为private修饰的来避免调用构造方法public static Singleton getInstance(){return instance;}
}

在多线程情况下,饿汉模式仍然是线程安全的:因为在创建这个类的时候,这个实例就已经有了,它的创建比主线程的启动都要早,所以在主线程中启动其他线程的时候,只需要读取已经创建好的线程就可以,我们前面说到,针对同一个变量进行修改的时候容易产生线程安全问题,但是现在只有读取操作,所以不存在线程安全问题.

1.2 懒汉模式

所谓懒汉模式,就突出一个字:,在第一次调用这个类的实例的时候才创建实例.如果没有被创建,引用一直为null.
单线程情况:

/*** 懒汉模式*/
class SingletonLazy{Object object = new Object();public static SingletonLazy instance = null;private SingletonLazy(){}public static SingletonLazy getInstance(){if (instance == null){//判断实例是否被创建过,如果被创建过,就不创建了,直接返回instance = new SingletonLazy();}return instance;}
}

多线程情况:
上述代码虽然说在单线程情况下不会出现什么bug,但是在多线程的情况下,就会难免出现bug,下面我们就通过画图的方式解释以下为什么会出现bug.
在这里插入图片描述
在线程1和线程2创建实例的时候,线程2的条件判断有可能在线程1创建实例执之前,此时线程1和线程2都会创建实例,从而违背了单例模式的初衷.这里产生线程安全问题的原因是操作不符合原子性.
那么为了避免在多线程中的安全问题,我们可以通过加锁的方式,把创建对象这个操作打包成一个原子的操作.

/*** 懒汉模式*/
class SingletonLazy{Object object = new Object();public static SingletonLazy instance = null;private SingletonLazy(){}public static SingletonLazy getInstance(){synchronized (instance.object){if (instance == null){//防止多个线程同时创建多个对象instance = new SingletonLazy();}}return instance;}
}

但是上述代码在一些方面还是显得有些赘余:

  1. 如果线程比较多,会加重锁竞争,在最初调用getInstance的时候会存在线程安全问题,但是后续调用,就只是读取操作,并不存在线程安全问题,所以此时上锁就是赘余,上锁本身也是一个开销比较大的操作.
  2. 存在内存可见性问题和指令重排序问题导致读取instance的偏差.
    为了解决上述问题,我们对上述代码进行优化:
/*** 懒汉模式*/
class SingletonLazy{Object object = new Object();public static volatile SingletonLazy instance = null;//加上volatile方式指令重排序和内存可见性引起的bugprivate SingletonLazy(){}public static SingletonLazy getInstance(){if (instance == null){//节省上锁的开支synchronized (instance.object){if (instance == null){//防止多个线程同时创建多个对象instance = new SingletonLazy();}}}return instance;}
}

这里两层判断的条件虽然是一样的,但是它们所起到的作用,却是大相径庭:
第一层判断是为了创建好实例之后,在后续获取实例的时候重复加锁,已节省系统开销.
第二层判断是为了多个线程如果都在创建好实例之前运行,防止这几个线程同时创建多个实例.

举例说明:
有请助教:荧,温迪,莱欧斯利,原神其他男性角色
现在温迪和莱欧斯利(两个线程)都知道了荧没有男朋友(没有创建实例),他们都突破了第一层if,于是开始竞争统一把锁.
在这里插入图片描述
假如现在风神巴巴托斯(线程1)更胜一筹:温迪就通过了里层的if,成为了荧的男朋友(创建了实例):
在这里插入图片描述
现在莱欧斯利(线程2)拿到了锁,但是被里层if排除在了外面,因为荧已经有了男朋友(实例已经创建了).
在这里插入图片描述
其他人听说了荧已经有了男朋友(已经创建了实例),纷纷散去了(被外层if排除在了外面).
在这里插入图片描述
在这里插入图片描述

2. 阻塞队列

2.1 什么是阻塞队列

阻塞队列是⼀种特殊的队列.也遵守"先进先出"的原则.
阻塞队列能是⼀种线程安全的数据结构(但是像我们前面学习的普通队列和优先级队列都是线程不安全的),并且具有以下特性:
• 当队列满的时候,继续⼊队列就会阻塞,直到有其他线程从队列中取⾛元素.
• 当队列空的时候,继续出队列也会阻塞,直到有其他线程往队列中插⼊元素.
阻塞队列的⼀个典型应⽤场景就是"⽣产者消费者模型".这是⼀种⾮常典型的开发模型.

拓展:消息队列
还有一种队列,叫做消息队列,首先通过topic这样的参数,对数据进行归类,出队列的时候,指定topic,每个topic下的所有数据先进先出,消息队列往往也带有阻塞特性.由于这种队列实在是太好用了,在部署服务器的时候,一般对这种队列会进行单独部署.

2.2 标准库中的阻塞队列

在Java标准库中内置了阻塞队列.如果我们需要在⼀些程序中使⽤阻塞队列.直接使⽤标准库中的即
可.
• BlockingQueue是⼀个接⼝.真正实现的类是LinkedBlockingQueue / ArrayBlockingQueue / PriorityBlockingQueue.
put方法用于阻塞式的入队列,take用于阻塞式的出队列.
• BlockingQueue也有offer,poll,peek等⽅法,但是这些⽅法不带有阻塞特性.

2.3 生产者消费者模型

⽣产者消费者模式就是通过⼀个容器来解决⽣产者和消费者的强耦合问题
⽣产者和消费者彼此之间不直接通讯, 而通过阻塞队列来进行通讯所以⽣产者⽣产完数据之后不⽤等待消费者处理,直接扔给阻塞队列,消费者不找⽣产者要数据,⽽是直接从阻塞队列⾥取.
在这里插入图片描述
生产者和消费者模型有以下好处:

  1. 阻塞队列就相当于⼀个缓冲区,平衡了⽣产者和消费者的处理能⼒.(削峰填谷)
    场景: 天猫11·11秒杀商品,在双十一的零点,阿里巴巴的服务器会接收到大量的支付请求,但是这样请求又是复杂的,很有可能服务器就被这样的一波请求冲垮了,这时候便会用到阻塞队列,让服务器慢慢处理这些指令.
    这样做可以有效进⾏"削峰",防⽌服务器被突然到来的⼀波请求直接冲垮.
    在这里插入图片描述

  2. 阻塞队列也能使⽣产者和消费者之间解耦.
    如果直接连接,服务器A和服务器B之间的逻辑关联会非常强,如果A或者B出现了bug或者出现了修改,另一个服务器就会被牵连到.这样的耦合性就比较高.我们希望通过阻塞队列来降低耦合性.让两个服务器和阻塞队列关联.

举例:过年包饺子
有请助教:空,荧
包饺子需要两步,擀皮,包馅,假如荧擀皮(生产者),空包馅(消费者),荧擀完皮会放在他们中间的盖帘上(阻塞队列).
在这里插入图片描述
假如荧擀皮慢,空包馅快,空在盖帘空的时候,空就会阻塞等待.
假如空包馅慢,荧擀皮快,空在盖帘满的时候,荧就会阻塞等待.

下面我们来用代码实现一个生产者消费者模型:

import java.util.concurrent.LinkedBlockingQueue;/*** 生产者与消费者模型*/
public class Demo23 {public static void main(String[] args) {BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<>();Thread thread = new Thread(()->{int count = 1;while (true){try {blockingQueue.put(count);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("put:" + count);count++;try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});Thread thread1 = new Thread(()->{int num = 0;while (true){try {num = blockingQueue.take();//当队列为空的时候就阻塞等待} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("take:" + num);}});thread.start();thread1.start();}
}

2.4 阻塞队列的实现

  • 通过"循环队列"的方式实现.
    循环队列描述
  • 使用synchronized进行加锁控制.
  • put插⼊元素的时候,判定如果队列满了,就进⾏wait.(注意,要在循环中进⾏wait.被唤醒时不⼀定
    队列就不满了,因为还有可能是其他的线程的interrupt唤醒了该线程
    ).
  • take取出元素的时候,判定如果队列为空,就进⾏wait.(也是循环wait)
/*** 实现阻塞队列*/
public class Block {public int size = 0;public int tail = 0;public int head = 0;public int[] item = new int[10];//默认循环队列的容积是10public int take() throws InterruptedException {//取队列元素synchronized (this){int ret = 0;while (size == 0){this.wait();}ret = item[head];//处队头元素head = (head+1)%item.length;size--;this.notify();//当出了一个元素之后,唤醒put的wait()return ret;}}public void put(int num) throws InterruptedException {synchronized (this){while (size == item.length){//队列满,阻塞等待this.wait();}item[tail] = num;tail = (tail+1)%item.length;size++;this.notify();//添加一个元素之后,唤醒take的阻塞}}
/**
*开始测试
**/public static void main(String[] args) {Block block = new Block();Thread thread = new Thread(()->{int num = 1;while (true){try {block.put(num);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("put:"+num);try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}num++;}});Thread thread1 = new Thread(()->{int ret = 0;while (true){try {ret = block.take();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("take:"+ret);}});thread.start();thread1.start();}
}

测试结果:
在这里插入图片描述

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

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

相关文章

BGP的基本配置

l 按照以下步骤配置BGP协议&#xff1a; 第1步&#xff1a;设备基本参数配置&#xff0c;AS内配置IGP确保内部网络连通性&#xff1b; l 配置IGP&#xff08;OSPF协议等&#xff09;路由解决peer对等体的源和目标IP之间连通性&#xff0c;确保peer之间TCP&#xff08;179&a…

【后端】python与django的开发环境搭建指南

安装Git 双击Git 客户端安装文件&#xff0c;在安装页面&#xff0c;单击“Next” 在安装路径选择页面&#xff0c;保持默认&#xff0c;单击“Next” 在功能组件选择页面&#xff0c;保持默认&#xff0c;单击“Next” 在开始菜单文件夹设置页面&#xff0c;保持默认&am…

好看到爆炸的弹窗公告源码

源码介绍 好看到爆炸的弹窗公告源码&#xff0c;源码由HTMLCSSJS组成&#xff0c;记事本打开源码文件可以进行内容文字之类的修改&#xff0c;双击html文件可以本地运行效果&#xff0c;也可以上传到服务器里面&#xff0c; 源码截图 源码下载 好看到爆炸的弹窗公告源码

【Elasticsearch】Elasticsearch 从入门到精通(二):基础使用

《Elasticsearch 从入门到精通》共包含以下 2 2 2 篇文章&#xff1a; Elasticsearch 从入门到精通&#xff08;一&#xff09;&#xff1a;基本介绍Elasticsearch 从入门到精通&#xff08;二&#xff09;&#xff1a;基础使用 &#x1f60a; 如果您觉得这篇文章有用 ✔️ 的…

SpringBoot+vue开发记录(二)

说明&#xff1a;本篇文章的主要内容为SpringBoot开发中后端的创建 项目创建: 1. 新建项目&#xff1a; 如下&#xff0c;这样简单创建就行了&#xff0c;JDK什么的就先17&#xff0c;当然1.8也是可以的&#xff0c;后面可以改。 这样就创建好了&#xff1a; 2. pom.xml…

光伏无人机:巡检无人机解决巡检难题

随着科技的飞速发展&#xff0c;无人机技术已经广泛应用于各个领域&#xff0c;其中光伏无人机在解决光伏电站巡检难题方面发挥了重要作用。光伏无人机以其高效、精准、安全的特点&#xff0c;为光伏电站的巡检工作带来了革命性的变革。 光伏电站通常位于广阔的户外场地&#x…

如何理解自然语言处理中的位置编码(Positional Encoding)

在自然语言处理和特别是在使用Transformer模型中,位置编码(Positional Encoding)是一个关键的概念。它们的作用是为模型提供序列中各个元素的位置信息。由于Transformer架构本身并不像循环神经网络(RNN)那样具有处理序列的固有能力,位置编码因此显得尤为重要。 为什么需…

7天入门Android开发之第1天——初识Android

一、Android系统 1.Linux内核层&#xff1a; 这是安卓系统的底层&#xff0c;它提供了基本的系统功能&#xff0c;如内存管理、进程管理、驱动程序模型等。安卓系统构建在Linux内核之上&#xff0c;借助于Linux的稳定性和安全性。 2.系统运行库层&#xff1a; 这一层包括了安卓…

一次违法网站的渗透经历

0x01 前言 在一次攻防演练中&#xff0c;我发现了一个有趣的渗透路径。在信息收集阶段&#xff0c;我注意到目标网站和用户资产网站共享相同的IP网段。这意味着它们可能在同一台服务器上托管&#xff0c;或者至少由同一家互联网服务提供商管理。这种情况为我们的渗透测试提供了…

Linux命令继续学习

which命令&#xff0c;找到各种命令程序所处在的位置 语法&#xff1a;which查找的命令 那么对于我们想查找其他类型文件所在的位置&#xff0c;我们可以用到find命令 find命令 选项为-name&#xff0c;表示按照文件名进行查找 find命令中通配符 find命令和前面rm命令一样&…

学习Rust第14天:HashMaps

今天我们来看看Rust中的hashmaps&#xff0c;在 std::collections crate中可用&#xff0c;是存储键值对的有效数据结构。本文介绍了创建、插入、访问、更新和迭代散列表等基本操作。通过一个计算单词出现次数的实际例子&#xff0c;我们展示了它们在现实世界中的实用性。Hashm…

xgp加速器免费 微软商店xgp用什么加速器

2001年11月14日深夜&#xff0c;比尔盖茨亲自来到时代广场&#xff0c;在午夜时分将第一台Xbox交给了来自新泽西的20岁年轻人爱德华格拉克曼&#xff0c;后者在回忆中说&#xff1a;“比尔盖茨就是上帝。”性能超越顶级PC的Xbox让他们趋之若鹜。2000年3月10日&#xff0c;微软宣…

ScriptableObject数据容器讲解

概述 是Unity提供的一个用于创建可重用的数据容器或逻辑的基类。 ScriptableObject 是继承自 UnityEngine.Object 的一个类&#xff0c;但与普通的 MonoBehaviour 不同&#xff0c;它不能附加到GameObject上作为组件。 相反&#xff0c;ScriptableObject 通常用于存储和管理…

意法半导体STM32F407VET6TR单片机优缺点、参数、应用和引脚封装

ST(意法半导体)的型号STM32F407VET6TR属于32位MCU微控制器&#xff0c;基于高性能的ArmCortex-M4 32位RISC核心&#xff0c;工作频率高达168MHz。单精度浮点单元(FPU)用于Cortex-M4核心&#xff0c;支持所有Arm单精度数据处理指令和数据类型。它还实现了一套完整的DSP指令和一个…

就业班 第三阶段(负载均衡) 2401--4.18 day2 LVS-DR模式

3、LVS/DR 模式 实验说明&#xff1a; 1.网络使用NAT模式 2.DR模式要求Director DIP 和 所有RealServer RIP必须在同一个网段及广播域 3.所有节点网关均指定真实网关 主机名ip系统用途client172.16.147.1mac客户端lvs-server172.16.147.154centos7.5分发器real-server1172.16.…

k8s日常动手实践 ~~ pod访问 pod请求 k8s api ~ 含新版带curl的busybox镜像

前言&#xff1a; 可以使用 Kubernetes API 获取集群信息。使用 Service Account&#xff08;SA&#xff09;进行身份验证&#xff0c;可以以安全的方式访问 Kubernetes API&#xff0c;而无需在 Pod 中使用明文凭据。 以下是一个使用 Service Account 访问 Kubernetes API 获…

Selenium IDE 常见错误笔记

错误1&#xff1a;Failed:Exceeded waiting time for new window to appear 2000ms 这个错误通常出现在第一次运行时&#xff0c;有两个原因&#xff1a; Firefox阻止了弹出式窗口&#xff0c;在浏览器设置里允许这个操作即可。 有些网站设置了反扒机制&#xff0c;脚本运行…

数据库并发控制思维导图+大纲笔记

思维导图 大纲笔记 多用户数据库系统 定义 允许多个用户同时使用的数据库系统特点 在同一时刻并发运行的事务数可达数百上千个多事务执行方式 事务串行执行交叉并发方式 单处理机系统同时并发方式 多处理机系统事务并发执行带来的问题 产生多个事务同时存取同一数据的情况可能…

Group Query Attention (GQA) 机制详解以及手动实现计算

Group Query Attention (GQA) 机制详解 1. GQA的定义 Grouped-Query Attention (GQA) 是对 Multi-Head Attention (MHA) 和 Multi-Query Attention (MQA) 的扩展。通过提供计算效率和模型表达能力之间的灵活权衡&#xff0c;实现了查询头的分组。GQA将查询头分成了G个组&#…

Jammy@Jetson Orin - Tensorflow Keras Get Started: 000 setup for tutorial

JammyJetson Orin - Tensorflow & Keras Get Started: 000 setup for tutorial 1. 源由2. 搭建环境2.1 安装IDE环境2.2 安装numpy2.3 安装keras2.4 安装JAX2.5 安装tensorflow2.6 安装PyTorch2.7 安装nbdiff 3. 测试DEMO3.1 numpy版本兼容问题3.2 karas API - model.compil…