详细介绍:javaEE:多线程,单列模式和生产者消费者模型

news/2026/1/21 12:23:27/文章来源:https://www.cnblogs.com/ljbguanli/p/19511013

详细介绍:javaEE:多线程,单列模式和生产者消费者模型

单列模式

单列模式的特点是:确保一个类在整个程序运行期间只会被创建一个实例,并且提供一个全局访问点供所有地方使用

单列模式的三要素
  1. 构造函数私有:不允许外部用new来创建对象
  2. 内部创建唯一实例:类自己创建并保存唯一实例
  3. 提供一个全局访问点:通常是一个静态方法getInstance()
单列模式的创建方法

最简单的单列模式示例(饿汉式):

class Singleton {
private static Singleton singleton = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return singleton;
}
}

代码解析:
第一行代码用来创建一个实例.由于被static修饰,所以这个实例会在类生成后的第一时间创建.是内部创建的唯一实例
第二行代码构造函数私有.不允许外部进行new Singleton()操作
第三行代码提供一个全局访问点,任何外部类都可以通过getInstance()方法来获得唯一实例
最简单的单列模式示例(懒汉式):

class SingletonLazy {
private static SingletonLazy singletonLazy = null;
public static SingletonLazy getInstance()() {
if (singletonLazy == null) {
return singletonLazy = new SingletonLazy();
}
return singletonLazy;
}
private SingletonLazy(){}
}

饿汉式和懒汉式的区别
饿汉式如同它名字一样,很饿,所以一上来就会创建实例
懒汉式如同它的名字一样,很懒,只在实例第一次被调用的时候才创建

懒汉式中的线程安全问题

对于懒汉式代码.因为它是在调用时就创建实例,所以当两个不同的线程同时调用了它,那么就会同时创建两个实例.这是不符合单列模式的预期的,所以我们需要修复线程安全问题
既然是预防同时创建新实例,我们只要对创建实例这个代码加锁就可以了吧

class SingletonLazy {
private static SingletonLazy singletonLazy = null;
public static SingletonLazy getInstance()() {
synchronized (SingletonLazy.class) {
if (singletonLazy == null) {
return singletonLazy = new SingletonLazy();
}
}
return singletonLazy;
}
private SingletonLazy(){}
}

这段代码正确实现了线程安全,不会创建多个实例.但是这个代码性能差.
为什么?
原因是每一次调用getInstance()时,即使对象已经创建了,也必须要进入加锁环节.要知道加锁的开销是非常大的.并且在多线程环境访问性能更会进一步下降(两个进程同时访问必有一个进程拿不到锁而等待)
所以我们应该在没有创建实例的时候进行加锁,如果已经创建了实例,那么就没必要再浪费性能加锁了
因此,优化后的代码如下

class SingletonLazy {
private static SingletonLazy singletonLazy = null;
public static SingletonLazy getInstance() {
if (singletonLazy == null) {
synchronized (SingletonLazy.class) {
if (singletonLazy == null) {
return singletonLazy = new SingletonLazy();
}
}
}
return singletonLazy;
}
private SingletonLazy(){}
}

这段代码完美实现了我们想要的逻辑.即只在需要没有创建实例时进行加锁保护,实例已创建就无需加锁保护
但我注意到这段代码

public static SingletonLazy getInstance() {
if (singletonLazy == null) {
synchronized (SingletonLazy.class) {
if (singletonLazy == null) {
return singletonLazy = new SingletonLazy();
}
}
}

同时出现了两个相同的if判断 if (singletonLazy == null).那是不是我们可以省略掉第二个if,这样还能减少一次if判断,进一次提高性能?
先说答案.不行.当我们删掉第二个个if,原代码会变成这样

public static SingletonLazy getInstance() {
if (singletonLazy == null) {
synchronized (SingletonLazy.class) {
return singletonLazy = new SingletonLazy();
}
}
}

其实这个段代码直接不满足线程安全了.原因是第一个if没加锁,在没创建实例的情况下,如果有两个线程同时进入了if.那么最后还是会创建两个实例.此时synchronized加锁的只是新建一个实例的过程,作用只是让两个线程先后创建新实例

  • volatile关键字的必要性
    对象初始化(new Singleton())不是原子操作.JVM会将这一步骤拆分为三个步骤:
  1. 分配内存
  2. 调用构造方法初始化对象
  3. 将instance指向这段内存
    但由于指令重排序,实际顺序可能会变成:
  4. 分配内存
  5. 将instance指向这段内存(此时对象未初始化)
  6. 调用构造方法初始化对象
    如果另一个线程此时恰好调用getInstance()方法.会返回一个"半成品对象".使用这个半成品对象可能会导致以下问题
  • 空指针异常
  • 业务逻辑异常
  • 难以发现的随机错误
    对于这样的问题,由于出现概率极小,所以排查难度非常大.一但发生就容易产生致命的问题
    而volatile就可以解决这个问题
    volatile的主要作用就是禁止指令重排
    当我们对instance修饰上volatile后,就不会出现上述问题了

只有在懒汉式中才需要使用volatile.饿汉式不需要,原因是饿汉式在类加载阶段就进行实例化,在类加载阶段中JVM为了保证线程安全,不会进行指令重排,所以不需要volatile

阻塞队列 Blocking Queue

队列的特点是先进先出.阻塞队列和队列的基本逻辑是一样的,也是先进先出,而它与队列的不同点就是会阻塞.具体来说

当队列满时,队列会自动阻塞等待,不会丢数据,也不会忙等

queue.put(item);
//如果此时queue中队列满了会一直等到队列有空时再向其添加item

当队列空时,队列会自动阻塞等待到队列右元素

queue.take();
//如果此时queue队列中没有元素会一直等待到有元素时再拿走元素

put()和take()方法做好了自动化,无须我们手动使用wait/notify
为了便于理解阻塞队列的put()和take()方法.我们可以自己写一个put()和take()

手动实现阻塞队列

首先我们得知道put()和take()的逻辑

  • 当队列满时,put()操作会被阻塞等待
  • 当队列空时,take()操作会被阻塞等待
    我们一开始需要做的就是创造一个环形数组用来临时存放数据
public class MyBlockingQueue {
private final int[] elem;
private int head = 0;
private int tail = 0;
private int size;
public MyBlockingQueue() {
this(10);
}
/***
* 可以手动指定要创建的数组大小
* @param length 数组大小
*/
public MyBlockingQueue(int length) {
elem = new int[length];
}
}

上述代码就是环形数组的实现.另外我添加了构造方法,通过构造方法我们能够自定义环形数组的大小

接下来就是配置put()和take()的逻辑了.既然是阻塞,那么一定得用上锁.这次我们使用的是更高级的ReentrantLock.因为我将会使用Condition来完成阻塞逻辑

Lock lock = new ReentrantLock();
Condition notEmpty = lock.newCondition();
Condition notFull = lock.newCondition();

上面这一段就是锁的逻辑.Condition就类似于synchronized的wait()/notify()方法.但COndition更加灵活.因为我们可以通过notEmpty来只管理这个条件的wait()/notify(),而不会影响到notFUll的wait()/notify()
简单来说就是通过Condition.可以将一个wait()/notify()分割成多个wait()/notify(),每一个wait()/notify()都可以只管理特定的条件
下面是put()逻辑的具体实现

public void put(int data) {
lock.lock();
try {
while(size == elem.length) {
notFull.await();
}
elem[tail++] = data;
if(tail == elem.length) {
tail = 0;
}
size++;
notEmpty.signal();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}

需要注意的是由于我们使用的是环形数组,所以别忘了维护数组不越界(就是if(tail == elem.length)的逻辑)
take()逻辑和put()几乎无差别

public int take() {
lock.lock();
try {
while(size == 0) {
notEmpty.await();
}
int tmp = elem[head++];
if(head == elem.length) {
head = 0;
}
size--;
notFull.signal();
return tmp;
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}

汇总代码如下

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MyBlockingQueue {
private final int[] elem;
private int head = 0;
private int tail = 0;
private int size;
public MyBlockingQueue() {
this(10);
}
/***
* 可以手动指定要创建的数组大小
* @param length 数组大小
*/
public MyBlockingQueue(int length) {
elem = new int[length];
}
Lock lock = new ReentrantLock();
Condition notEmpty = lock.newCondition();
Condition notFull = lock.newCondition();
public void put(int data) {
lock.lock();
try {
while(size == elem.length) {
notFull.await();
}
elem[tail++] = data;
if(tail == elem.length) {
tail = 0;
}
size++;
notEmpty.signal();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
public int take() {
lock.lock();
try {
while(size == 0) {
notEmpty.await();
}
int tmp = elem[head++];
if(head == elem.length) {
head = 0;
}
size--;
notFull.signal();
return tmp;
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
}
BlockingQueue 提供的四类方法
add() remove() 队列的原始方法

add()和remove()和原本的队列方法一模一样,使用方式也是相同的.当队列满时add()会抛出异常:当队列空时take()也会抛出异常

offer() poll() 队列的原始方法

offer()和poll()和原本的队列方法一模一样.当队列为满时使用offer()会返回false:当队列为空时使用poll()方法会返回null

put() take() 阻塞等待

当队列为满时使用put()会阻塞等待,直到有空时才会向其添加元素:当队列为空时使用take()方法也是同理

offer(data,time,unit) poll(time.unit) 超时等待

unit是时间单位
超过指定时间还未完成对应添加/提取会返回false;如果在指定时间完成对应添加/提取会返回true

实现生产者消费者模型

在实现生产者消费者模型之前我们应该知道它是干什么的,为什么要使用它

生产者消费者模型是什么?

通过生产者消费者模型可以解决多线程之间如何安全且高效的传递数据的问题,让生产数据的线程和消费的线程能够解耦,并行,互不干扰

为什么要使用生产者消费者模型?
  1. 当生产速度和消费速度不一致时:
    生产者可能生产更快,消费者处理慢,导致数据堆积,内存爆炸
    而当消费者快生产者慢时,消费者经常拿到空数据
    而生产者消费者模型正好解决了这个问题:
  • 队列满了,让生产者阻塞,停止生产
  • 队列空了,消费者阻塞,停止处理数据
    通过这样的调节,我们就能解决生产和消费速率不均的问题
  1. 线程间可以更安全的传递数据
    当多个线程同时操作同一组数据时,容易出现线程安全问题
    而使用BlockingQueue时会自动加锁,不需要我们手动使用synchronized

还有一些其它的好处(解耦合,防止cpu空转等等)

  • 当然不光只有好处,也有一部分代价:
  1. 效率降低:加锁解锁,阻塞等待等等,都会降低代码的执行效率
  2. 系统结构更复杂,运维成本增加:多了一个生产者消费者模型,多了一段代码
    但总的来说,在大部分情况下使用生产者消费者模型都是利大于弊的
生产者消费者模型的具体实现

因为前文已经写了一个MyBlockingQueue类,所以我们可以直接使用自己手写的阻塞队列来实现一个生产者消费者模型,非常简单

public class Main {
public static void main(String[] args) {
MyBlockingQueue queue = new MyBlockingQueue();
Thread t1 = new Thread(() -> {//生产者
for (int i = 0; i < 500; i++) {
queue.put(i);
System.out.println("生产元素" +
i);
}
});
Thread t2 = new Thread(()-> {//消费者
for (int i = 0; i < 500; i++) {
System.out.println("消费元素" +
queue.take());
}
});
t1.start();
t2.start();
}
}

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

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

相关文章

AF594标记的Streptavidin,一种基于生物素-链霉亲和素体系的AF405荧光探针

【试剂简介】英文名称&#xff1a;Streptavidin, AF594 conjugate&#xff0c;AF594 Streptavidin&#xff0c;AF594标记的Streptavidin&#xff0c;Alexa Fluor594 Streptavidin中文名称&#xff1a;AF594标记的链霉亲和素&#xff0c;链霉亲和素偶联AF594&#xff0c;链霉亲和…

CORS配置避坑指南,90%开发者忽略的跨域安全细节大公开

第一章&#xff1a;Java解决跨域问题CORS配置 在现代Web开发中&#xff0c;前端与后端分离架构日益普及&#xff0c;跨域资源共享&#xff08;CORS&#xff09;成为必须面对的问题。当浏览器发起的请求目标与当前页面源不同时&#xff0c;会触发同源策略限制&#xff0c;导致请…

字符串判空的5种方式大比拼(哪种效率最高?)

第一章&#xff1a;Java判断字符串是否为空的最佳实践 在Java开发中&#xff0c;判断字符串是否为空是一个常见但关键的操作。不正确的处理方式可能导致空指针异常&#xff08;NullPointerException&#xff09;&#xff0c;影响程序的稳定性。因此&#xff0c;采用安全且可读性…

线性注意力(Linear Attention,LA)学习

定义:采用矩阵乘法结合律的特点,所设计的一种\(\mathcal{O}(n)\)时间复杂度的注意力机制 一、softmax注意力机制 设输入特征\(x\)大小为\(NF\),其是由\(N\)个维度为\(F\)的特征向量构成的序列(往往\(N\gg F\)) Tr…

Parquet 入门详解:深入浅出全解析

https://blog.csdn.net/qq_28369007/article/details/148840528 Parquet 入门详解:深入浅出全解析

实测总结:AI生成PPT的6个常见坑,新手必看

从满怀期待到被坑无语&#xff0c;这份避坑指南或许能帮你省下大量时间。大家好&#xff0c;最近一年AI生成PPT的风很大&#xff0c;相信不少朋友都尝试过。但用完之后&#xff0c;可能不少人和我一样&#xff0c;从“终于能解放了”的兴奋&#xff0c;变成了“还不如我自己做”…

AF430标记的Streptavidin,链霉亲和素AF430偶联物:光谱特性、实验应用与操作要点

【试剂名称】英文名称&#xff1a;Streptavidin, AF430 conjugate&#xff0c;AF430 Streptavidin&#xff0c;AF430标记的Streptavidin&#xff0c;Alexa Fluor430 Streptavidin中文名称&#xff1a;AF430标记的链霉亲和素&#xff0c;链霉亲和素偶联AF430&#xff0c;链霉亲和…

uniapp vue h5小程序奶茶点餐纯前端hbuilderx

内容目录 一、详细介绍二、效果展示1.部分代码2.效果图展示 三、学习资料下载 一、详细介绍 uniapp奶茶点餐纯前调试视频.mp4链接: uniapp奶茶点餐纯前调试视频注意事项: 本店所有代码都是我亲测100%跑过没有问题才上架 内含部署环境软件和详细调试教学视频 代码都是全的&…

ubuntu系统下,vim编辑时候,如何显示行数

编辑 ~/.vimrc 文件(如没有则创建): vim ~/.vimrc 添加以下内容 set number autocmd InsertEnter * :set norelativenumber autocmd InsertLeave * :set relativenumber 保存退出

空指针不再怕,Java字符串判空实战技巧全解析

第一章&#xff1a;Java字符串判空的核心概念与重要性 在Java开发中&#xff0c;字符串是最常用的数据类型之一。由于其频繁参与业务逻辑判断、数据校验和用户交互&#xff0c;对字符串进行判空操作成为保障程序健壮性的关键步骤。未正确处理null值或空字符串&#xff0c;极易引…

6.3 密钥隐身术:Sealed-Secrets 与 Vault 的 K8s 密钥管理之道

6.3 密钥隐身术:Sealed-Secrets 与 Vault 的 K8s 密钥管理之道 1. 引言:Base64 ≠ 加密 K8s Secret 天然“弱保护”:默认以 Base64 存储于 Etcd,未开启 at-rest 加密时属于明文。密钥管理的目标是:密钥不落盘、最小暴露、可审计、可轮换。 2. Sealed Secrets:把密钥“安…

6.4 守门员机制:使用 Kyverno 实施 K8s 准入控制与安全策略

6.4 守门员机制:使用 Kyverno 实施 K8s 准入控制与安全策略 1. 引言:把“应当如此”写成策略 准入控制是最后一道关口。把“安全与规范”从检查清单,变为可执行的策略。Kyverno 使用原生 YAML 模式,无需学习 Rego 即可编写策略,适合大规模推广。 2. Kyverno 策略类型 Va…

单细胞质量控制常见指标的解读学习

常见指标 是什么?nFeature_RNA 一个细胞表达了多少种不同的基因nCount_RNA 一个细胞里检测到的所有RNA分子(UMI)总数percent.mt 细胞中线粒体基因的RNA占比percent.HB 细胞中血红蛋白基因的RNA占比nFeature_RNA(左…

Java单例模式选型决策树(附HotSpot 8–17实测数据):哪种实现吞吐量高37%、内存占用低2.1倍?

第一章&#xff1a;Java单例模式选型的核心挑战 在高并发与复杂系统架构中&#xff0c;单例模式作为最常用的设计模式之一&#xff0c;其正确实现直接影响系统的稳定性、性能和可维护性。尽管看似简单&#xff0c;但在实际应用中&#xff0c;开发者常面临线程安全、延迟加载、反…

【Java百万级Excel导出性能优化实战】:20年架构师亲授7大内存与IO瓶颈突破方案

第一章&#xff1a;百万级Excel导出的典型性能瓶颈全景图在处理百万级数据量的Excel导出任务时&#xff0c;系统往往面临严峻的性能挑战。传统方式依赖内存加载全部数据后写入文件&#xff0c;极易引发内存溢出、响应超时和CPU过载等问题。理解这些瓶颈的成因与表现形式&#x…

探讨汽车变速器连接器,青宸精密科技提供的产品性价比哪家高?

随着新能源汽车产业向智能化、集成化、高压化升级,汽车变速器作为动力传递核心部件,其内部连接器的可靠性直接决定整车动力响应与行驶安全。本文围绕汽车变速器连接器的选型痛点,结合深圳市青宸精密科技有限公司的行…

依赖冲突频繁爆发?掌握这4种高阶策略,轻松实现项目稳定构建

第一章&#xff1a;依赖冲突频繁爆发&#xff1f;重新认识Maven的依赖解析机制 在大型Java项目中&#xff0c;依赖冲突是开发过程中最常见的痛点之一。Maven作为主流的构建工具&#xff0c;其依赖解析机制直接影响着最终打包结果的稳定性和可预测性。理解Maven如何选择和解析依…

盘点深圳青宸精密科技可提供的汽车变速器连接器,专业供应企业有哪些?

问题1:汽车变速器连接器加工厂的专业度体现在哪些方面?如何判断是否值得合作? 汽车变速器连接器是汽车动力传输系统的神经节点,其专业度直接决定了车辆换挡平顺性、信号传输稳定性乃至行车安全。判断加工厂是否专业…

还在为提取链接发愁?1个正则搞定所有网页URL抓取场景

第一章&#xff1a;正则表达式提取网页链接的核心价值 在现代Web数据处理中&#xff0c;从非结构化HTML文本中高效提取有效链接是信息采集、搜索引擎优化和自动化测试的关键环节。正则表达式作为一种强大的文本匹配工具&#xff0c;能够在不依赖完整解析器的情况下快速定位URL模…

投影机出租服务对比:2026年值得考虑的厂家,8000流明投影机/8K投影机/投影仪出租,投影机出租供应厂家哪家好

在数字化展示与沉浸式体验日益成为主流的今天,无论是大型商业发布会、高端艺术展览,还是文旅夜游项目,高品质的视觉呈现已成为活动成功的关键一环。投影机出租服务,凭借其灵活的成本控制、免维护的便捷性以及获取前…