JavaEE初阶-多线程4

文章目录

  • 一、单例模式
    • 1.1 饿汉模式
    • 1.2 懒汉模式
  • 二、阻塞队列
    • 1.1 生产者消费者模型
      • 1.1.1 现实生活举例
      • 1.1.2 生产者消费模型的两个优势
        • 1.1.2.1 解耦合
        • 1.1.2.2 削峰填谷
    • 1.2 阻塞队列代码
      • 1.2.1 使用java标准库的阻塞队列实现生产者消费者模型
      • 1.2.2 实现自己的阻塞队列


一、单例模式

单例模式是一种经典的设计模式,指的是对于整个进程中的某个类,有且仅有一个对象。单例模式有两种写法,分别为饿汉模式和懒汉模式。

1.1 饿汉模式

代码如下:

package Thread;class Singleton {public static Singleton instance = new Singleton();public static Singleton getInstance() {return instance;}private Singleton() {}
}public class Demo31 {public static void main(String[] args) {Singleton s1 = Singleton.getInstance();Singleton s2 = Singleton.getInstance();System.out.println(s1 == s2);}
}

为什么叫饿汉模式,因为在这个单例类中,在类的加载的时候直接定义并且建立了一个实例对象,这就凸显了“饿”的思想。因为在类的初始化时就已经建立好一个对象了,所以后续如果在多线程的情况下使用getInstance方法就不会设计线程安全的问题,因为此时只是一个多线程读取同一个变量的问题。然后我们还发现,单例类中的构造函数被private修饰,这时为了避免在类外去实例化其它的对象,从而达到“单例”的效果。

1.2 懒汉模式

在计算机这个领域当中,“懒”往往不是个贬义词,懒代表着高效率,懒汉模式不是在类初始化时就直接创建实例,而是等到需要使用实例的时候才去创建,这样当不需要使用实例时就能省下创建实例的开销。
代码如下:

package Thread;class Singleton1 {public static Singleton1 instance = null;public static Singleton1 getInstance() {if (instance == null) { instance = new Singleton1();}return instance;}private Singleton1() {}
}public class Demo32 {public static void main(String[] args) {Singleton1 s1 = Singleton1.getInstance();Singleton1 s2 = Singleton1.getInstance();System.out.println(s1 == s2);}
}

上述代码是一个懒汉模式的简单代码,我们不难想到它是线程不安全的。因为在多线程的环境下去调用getInstance这个方法相当于在多线程的环境下来修改同一个变量,就会出现线程安全问题。
在这里插入图片描述
如图,如果两个线程以这样的方式执行代码,线程1执行到if后线程2立马也执行到if,然后线程1创建实例,线程2也跟着创建实例,此时进程中就创建了两个实例,出现了安全问题。不要意味多创建一个实例没什么大不了的,单例模式的应用场景如下:

例一:
比如你写的服务器要从硬盘上加载100G数据到内存中,要写一个类来封装以上的加载操作,并且写一些获取或处理数据的逻辑,这样的类就应该是单例的,一个实例就管理100G的数据,建立多个实例机器也吃不消。
例二:
服务器可能会涉及一些配置项,代码中也需要专门的类来管理这些配置,需要加载配置数据到内存以供其它代码使用。这样的类也应该是单例的。因为配置是唯一的,如果有多个实例,那应该以哪个为准?

因此多创建一个实例,可能这个实例会管理100G的数据,会造成很大开销。下面我们回归正题,既然有线程安全的问题,那么我们就要去解决,给代码加锁。代码修改如下:

package Thread;class Singleton1 {public static Object locker = new Object();public static Singleton1 instance = null;public static Singleton1 getInstance() {synchronized (locker) {if (instance == null) { //避免在多线程情况下重复创建对象,造成线程安全问题instance = new Singleton1();}}return instance;}private Singleton1() {}
}public class Demo32 {public static void main(String[] args) {Singleton1 s1 = Singleton1.getInstance();Singleton1 s2 = Singleton1.getInstance();System.out.println(s1 == s2);}
}

这样就能避免前面的问题。当线程1进入if此时线程2是不可以的,因为加锁了,线程2直接堵塞。但是当实例创建好之后代码中就不涉及线程安全问题了,就是多个线程去读一个变量,同时加锁又是一个重量级得操作会影响到代码执行的效率,所以我们给getInstance方法的代码的锁之外再加上一层判断语句,如果已经有实例对象了就直接返回对象即可,无需再去执行后面的操作。代码修改如下:

package Thread;class Singleton1 {public static Object locker = new Object();public static volatile Singleton1 instance = null;public static Singleton1 getInstance() {if (instance == null) { //避免已经建立了对象重新上锁浪费性能,直接返回对象即可synchronized (locker) {if (instance == null) { //避免在多线程情况下重复创建对象,造成线程安全问题instance = new Singleton1();}}}return instance;}private Singleton1() {}
}public class Demo32 {public static void main(String[] args) {Singleton1 s1 = Singleton1.getInstance();Singleton1 s2 = Singleton1.getInstance();System.out.println(s1 == s2);}
}

此时完成懒汉单例模式代码编写。我们可以看到我们在instance变量声明时加上了volatile关键字,这是为了避免编译器优化策略中的内存可见性问题,避免在线程1中创建实例对象线程2中感知不到,但是这是很小概率是为了以防万一。另外加上volatile也可以避免另一种编译器优化策略即指令重排序造成的问题。

指令重排序:
编译器比较智能,会将从代码中得到的二进制指令序列的顺序进行调整从而提高效率,重排序的前提就是结果不会发生改变,这种策略在单线程的情况下当然没有问题,但是在多线程的情况下就可能会出现问题。

对于instance = new Singleton1();这段代码可以分为三步,第一步就是申请空间,第二步初始化空间,第三步是将空间的地址赋给instance这里的引用,本来是这样的执行顺序,但是经过编译器优化策略即指令重排序,执行顺序变为了一三二。
在这里插入图片描述

如图,如果经过指令重排序后指令执行顺序为一三二,那么在线程1完成第一步和第三步即申请完空间并且赋给instance引用后线程2开始执行,因为此时instance已经被赋值并非为null,所以后面会直接返回instance,但是此时的instance是未被初始化的空间,因此对其进行操作肯定会出错。为了避免这种指令重排序造成的线程安全问题,就在instance前加上volatile,其它变量也是一样。

单例模式补充扩展:
单例模式确保反射安全,即使使用反射也无法破坏单例模式特性。
单例模式确保序列化下安全,即使使用java标准库中的序列化特性也无法破坏单例特性。
对象转为二进制字符串->序列化
二进制字符串转为对象->反序列化

二、阻塞队列

相对于优先级队列和普通队列,阻塞队列是线程安全的并且带有阻塞功能。当队列为空时如果要执行出队列的操作,那么出队列操作就会阻塞直至队列不为空。当队列满的时候也是一样,会阻塞入队列的操作直至队列不为满。BlockingQueue这就是java标准库提供的阻塞队列的接口。
与阻塞队列相似的还有消息队列,消息会通过topic对数据进行归类,每个类别都是一个阻塞队列,指定topic,每个topic下的数据都是先进先出的。因为消息队列这样的数据结构太好用了,所以在实际开发中往往会将消息队列封装成单独的服务器程序,这样的服务器程序也被称为消息队列。消息队列在实际开发中经常用于实现生产者消费者模型。普通的阻塞队列也可以实现生产者消费者模型,主要是看场景,如果是在一个进程中,那么使用阻塞队列即可,如果是需要在分布式系统中实现生产者消费者模型,那么就需要消息队列。

1.1 生产者消费者模型

生产者消费者模型是用来解决问题的经典方案。

1.1.1 现实生活举例

在这里插入图片描述
如图右三个滑稽包饺子,滑稽A负责擀饺子皮,滑稽B和C负责包饺子,滑稽A将饺子皮擀好了放在中间的盘子上,然后滑稽B和C拿盘子上的饺子皮来包饺子。在这个过程中A就是生产者,B和C就是消费者,A生产数据,B使用数据,中间的盘子是一个阻塞队列,当盘子中为空时相当于队列为空,此时B和C就要堵塞,要等待盘子中有饺子皮。当盘子被饺子皮装满,此时A就要阻塞,不能再放入饺子皮了。

1.1.2 生产者消费模型的两个优势

1.1.2.1 解耦合

在这里插入图片描述
以上是一个很简单的示意图,A和B之间相互调用,那么A当中就需要包含和B相关的代码或逻辑,相同的B当中也需要包含和A相关的代码或逻辑,这样A和B之间就具有了一定的耦合,当修改A时,B也要跟着改变,当修改B时也是一样A也要跟着改变。
在这里插入图片描述
如图当我们引入消息队列后A就不需要去直接和B打交道,A以及B直接和消息队列进行交互,这样A和B之间的互相影响很小。当我们要多引入一个C时,也不需要让A以及B修改任何代码,直接让C和消息队列交互即可,这样就达到了解耦合的效果。

1.1.2.2 削峰填谷

客户端发来的请求,个数多少无法预知,遇到某些突发事件可能会导致客户端对服务器的请求数量激增。一般来说接收方的处理逻辑相对复杂,当需求突然变多,服务器可能处理不过来导致直接挂掉。

在这里插入图片描述
在正常情况下都是A接收到一次请求就发送一条请求给B,因为B的处理逻辑通常比A复杂,因此当请求过多消耗的资源超过机器的上限,B就会挂掉。如图加入消息队列后就将B给保护起来了,此时B不需要考虑请求有多少,它可以按照自己的节奏来处理。
显然加入阻塞队列也有缺点,处理的速度变慢了。因为多了一次周转也就是网络通信,对于要求响应速度非常高的场景是不适用的。

1.2 阻塞队列代码

java标准库中的阻塞队列接口及其对应的阻塞队列的类如下。
在这里插入图片描述在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
其中需要注意的是LinkedBlockingQueue类是自动扩容的,因此只会再队列为空时对出队操作阻塞,不会阻塞入队操作。

1.2.1 使用java标准库的阻塞队列实现生产者消费者模型

代码如下:

package Thread;import java.util.concurrent.*;public class Demo34 {public static void main(String[] args) throws InterruptedException {ArrayBlockingQueue<Integer> blockingQueue=new ArrayBlockingQueue<>(10);Thread t2 = new Thread(() -> {int count = 1;while (true) {System.out.println("t2生产:" + count);try {blockingQueue.put(count);} catch (InterruptedException e) {throw new RuntimeException(e);}try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}count++;}});Thread t3 = new Thread(() -> {try {while (true) {System.out.println("t3消费:" + blockingQueue.take());Thread.sleep(1000);}} catch (InterruptedException e) {throw new RuntimeException(e);}});t2.start();Thread.sleep(1000);t3.start();}}

这段代码因为设置了进程中的放入时间的间隔,所以每次生产者线程t2数据一生成就被t3线程消费掉了,代码执行的效果如下:
在这里插入图片描述

1.2.2 实现自己的阻塞队列

代码如下:

package Thread;import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingDeque;class MyArrayBlockingQueue {private volatile int head = 0;private volatile int tail = 0;private volatile int len = 0;private String[] blockQueue;private int size;public MyArrayBlockingQueue(int capacity) {blockQueue = new String[capacity];size = capacity;}public void put(String str) throws InterruptedException {synchronized (this) {//加入while是因为再次判断 因为interrupt也可以唤醒wait 所以要杜绝这种可能。while (len == size) {this.wait();// 这里处理异常使用了throws 如果这里被interrupt方法唤醒那么函数直接结束执行}blockQueue[tail] = str;tail++;if (tail >= blockQueue.length) {tail = 0;}len++;this.notify();}}public String take() throws InterruptedException {synchronized (this) {while (len == 0) {this.wait();}String ret = blockQueue[head];head++;if (head >= size) {head = 0;}len--;this.notify();return ret;}}
}public class Demo35 {public static void main(String[] args) throws InterruptedException {MyArrayBlockingQueue myArrayBlockingQueue = new MyArrayBlockingQueue(1000);Thread t1 = new Thread(() -> {try {int count = 1;while (true) {System.out.println("生产:" + count);myArrayBlockingQueue.put(count + "");count++;Thread.sleep(1000);}} catch (InterruptedException e) {throw new RuntimeException(e);}});Thread t2 = new Thread(() -> {while (true) {try {System.out.println("消费:" + myArrayBlockingQueue.take());// Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});t1.start();t2.start();}
}

这里代码中使用了一个数组来实现了一个循环的阻塞的队列,大部分逻辑和循环队列是相似的,但是有一些部分不一样。代码中给put和take函数中都加上锁,因为这里要达到阻塞的效果就需要使用wait使得线程进入waiting状态,wait必须要在锁中使用。当使用put方法发现队列已经满了线程就要进入waiting状态,此时这里的判断条件是while循环,因为wait可以使用interrupt方法唤醒,所以使用循环多次判断,当某个线程调用了take方法拿走了队列中的数据,之后会直接唤醒这里put方法中的wait,take方法的思路也和put方法中一致。然后代码中的变量都加上了volatile为了以防万一避免内存可见性以及指令重排序的问题。

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

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

相关文章

30年赚1000亿美元--“量化之王”和他最传奇的基金“大奖章”的秘密

文艺复兴是华尔街最成功、最神秘的机构之一。从1988-2018年的30年里&#xff0c;文艺复兴仅向内部员工开放的旗舰基金“大奖章”累计创造了超过1000亿美元的收益&#xff0c;年均回报率高达39%。作为对比&#xff0c;同期“股神”巴菲特的年均回报率为20.5%。 而且&#xff0c;…

【Linux】-IP地址、主机名配置[5]

目录 一、IP和主机名 1、IP地址 2、特殊IP地址 3、主机名 4、在Linux中修改主机名 5、配置主机名映射 二、虚拟机配置固定IP 1、为什么需要固定IP 2、在VMware Workstation中配置固定ip 一、IP和主机名 1、IP地址 每一台联网的电脑都会有一个地址&#xff0c;用于和…

大模型面试常考知识点1

文章目录 1. 写出Multi-Head Attention2. Pre-Norm vs Post-Norm3. Layer NormRMS NormBatch Norm 4. SwiGLU从ReLU到SwishSwiGLU 5. AdamW6. 位置编码Transformer位置编码RoPEALibi 7. LoRA初始化 参考文献 1. 写出Multi-Head Attention import torch import torch.nn as nn …

QT6 android程序界面强制横屏显示不旋转

QT6开发的Android程序有时候旋转后程序会变形&#xff0c;比如想让其固定位横屏显示&#xff0c;就需要进行特殊设置&#xff0c;本文提供一种简便的设置方法。 一.AndroidManifest.xml文件介绍 Android的Manifest.xml文件是一个重要的配置文件&#xff0c;用于描述应用程序的…

2024最新从0部署Django项目(nginx+uwsgi+mysql)

云服务器 我这里用的是腾讯云免费试用的2H4Gcentos服务器&#xff08;后升级为2H8G&#xff0c;保险一点提高内存&#xff09; 因为网上很多关于django部属的教程都是宝塔啊&#xff0c;python版本控制器啊这种的&#xff0c;我也误打误撞安装了宝塔面板&#xff0c;但这里我…

浅谈运维数据安全

在数字化日益深入的今天&#xff0c;运维数据安全已经成为企业信息安全体系中的核心要素。运维工作涉及到企业信息系统的各个方面&#xff0c;从硬件维护到软件升级&#xff0c;从网络配置到数据备份&#xff0c;无一不需要严谨的数据安全保障措施。本文将从运维数据安全的重要…

民航电子数据库:select查询时部分字段缺失

目录 前言异常排查原因解决使用systemPath标签引入本地Jar包后无法打包 前言 1、对接民航电子数据库 2、框架为shardingsphere caedb mybatis 3、部分SQL查询时&#xff0c;会出现字段缺失的情况 4、查看日志打印出来的SQL&#xff0c;字段并未缺失 异常 这里省略SQL语句…

FreeRTOS事件组

什么是事件标志组? 事件标志位 :表明某个事件是否发生,联想:全局变量 flag 。通常按位表示,每一个位表示一个事件(高8 位不算) 事件标志组 是一组事件标志位的集合, 可以简单的理解事件标志组,就是一个整数。 事件标志组本质是一个 16 位或 32 位无符号的数据类型…

鸿蒙开发接口Ability框架:【DataAbilityHelper模块(JS端SDK接口)】

DataAbilityHelper模块(JS端SDK接口) 说明&#xff1a; 本模块首批接口从API version 7开始支持。后续版本的新增接口&#xff0c;采用上角标单独标记接口的起始版本。 本模块接口仅可在FA模型下使用。 使用说明 使用前根据具体情况引入如下模块 import featureAbility from …

Excel中实现md5加密

1.注意事项 (1)在Microsoft Excel上操作 (2)使用完&#xff0c;建议修改的配置全部还原&#xff0c;防止有风险。 2.准备MD5宏插件 MD5加密宏插件放置到F盘下&#xff08;直接F盘下&#xff0c;不用放到具体某一个文件夹下&#xff09; 提示&#xff1a;文件在文章顶部&…

【教程向】从零开始创建浏览器插件(三)解决 Chrome 扩展中弹出页面、背景脚本、内容脚本之间通信的问题

第三步&#xff1a;解决 Chrome 扩展中弹出页面、背景脚本、内容脚本之间通信的问题 Chrome 扩展开发中&#xff0c;弹出页面&#xff08;Popup&#xff09;、背景脚本&#xff08;Background Script&#xff09;、内容脚本&#xff08;Content Script&#xff09;各自拥有独立…

互联网轻量级框架整合之HibernateMyBatis

持久层框架 Hibernate 假设有个数据表&#xff0c;它有3个字段分别是id、rolename、note, 首先用IDEA构建一个maven项目Archetype选择org.apache.maven.archetypes:maven-archetype-quickstart即可&#xff0c;配置如下pom <project xmlns"http://maven.apache.org/…

攻防世界-web-unseping

题目 知识点 PHP代码审计PHP序列化和反序列化PHP中魔术方法命令执行绕过方式 解读源码 <?php highlight_file(__FILE__);class ease{private $method;private $args;function __construct($method, $args) {$this->method $method;$this->args $args;}function …

【算法】动态规划之背包DP问题(2024.5.11)

前言&#xff1a; 本系列是学习了董晓老师所讲的知识点做的笔记 董晓算法的个人空间-董晓算法个人主页-哔哩哔哩视频 (bilibili.com) 动态规划系列 【算法】动态规划之线性DP问题-CSDN博客 01背包 步骤&#xff1a; 分析容量j与w[i]的关系&#xff0c;然后分析是否要放…

户籍管理系统的设计与实现

【免费】户籍管理系统的设计与实现.zip资源-CSDN文库https://download.csdn.net/download/JW_559/89296790 ​​​​​​​户籍管理系统的设计与实现 摘 要 当今社会人们生活质量越来越高&#xff0c;人们对生活品质的追求不断提升&#xff0c;对于孩子求学&#xff0c;变更住…

Selenium中的隐式等待和显式等待

在Selenium中&#xff0c;“等待”在执行测试中起着重要作用。在本文中&#xff0c;您将学习Selenium中“隐式”和“显式”等待的各个方面。 在本文中&#xff0c;您将学习到 1. 为什么我们需要在selenium中等待&#xff1f; 2. 隐瞒等待 3. 明确等待 4. 流利的等待 为什么…

Join优化规则及应用层BI系统实践

目录 一、背景 二、查询优化器概述​编辑 2.1 System R Optimizer 2.2 Volcano Optimizer 2.3 Cascade Optimizer 三、Join相关优化规则 3.1 JoinReorder 3.1.1 少量表的Reorder 3.1.2 大量表的Reorder 3.1.3 星型模型的Reorder 3.2 外连接消除 3.3 Join消除 3.4 谓…

UDP多播

1 、多播的概念 多播&#xff0c;也被称为组播&#xff0c;是一种网络通信模式&#xff0c;其中数据的传输和接收仅在同一组内进行。多播具有以下特点&#xff1a; 多播地址标识一组接口&#xff1a;多播使用特定的多播地址&#xff0c;该地址标识一组接收数据的接口。发送到多…

[Vision Board创客营]--使用openmv识别阿尼亚

文章目录 [Vision Board创客营]使用openmv识别阿尼亚介绍环境搭建训练模型上传图片生成模型 使用结语 [Vision Board创客营]使用openmv识别阿尼亚 &#x1f680;&#x1f680;五一和女朋友去看了《间谍过家家 代号&#xff1a;白》&#xff0c;入坑二刺螈&#xff08;QQ头像也换…

小程序地理位置接口申请教程来啦4步学会

小程序地理位置接口有什么功能&#xff1f; 如果我们提审后驳回理由写了“当前提审小程序代码包中地理位置相关接口( chooseAddress、getLocation )暂未开通&#xff0c;建议完成接口开通后或移除接口相关内容后再进行后续版本提审”&#xff0c;如果你也碰到类似问题&#xff…