JavaEE初阶Day 10:多线程(8)

目录

  • Day 10:多线程(8)
    • 单例模式
    • 阻塞队列
      • 1. 生产者消费者模型
        • 1.1 生产者消费者模型解耦合
        • 1.2 生产者消费者模型削峰填谷
      • 2. 生产者消费者代码
      • 3. 阻塞队列实现

Day 10:多线程(8)

单例模式

单例模式:某个类在进程中只能有唯一实例,需要一定的编程技巧,作出限制,一旦代码写的有问题,创建了多个实例,直接编译报错

  • 饿汉模式:程序运行的时候,就立即创建实例

  • 懒汉模式:首次使用的时候,才创建实例

    • 加锁:把if和new包裹起来

    • 双重if

    • 给变量上加上volatile

      可能会涉及到内存可见性问题:t1线程修改了Instance引用,t2有可能读不到(概率应该比较小),加上volatile是为了万无一失,另一方面,加上volatile也能够解决指令重排序引起的线程安全问题

指令重排序:也是编译器的一种优化策略,编译器优化有很多种策略,比如把读内存优化到读寄存器、指令重排序、循环展开、条件分支预测等

写的代码最终编译成了一系列的二进制指令,正常来说,CPU是按照顺序,一条一条地执行,但是编译器比较智能,会根据实际情况,生成的二进制指令的执行顺序可能和最初写代码的顺序存在差别,调整顺序的最主要的目的就是为了提高效率(前提是保证逻辑是等价的)

  • 指令重排序的前提一定是重新排序之后,逻辑和之前等价
  • 单线程下,编译器进行指令重排序的操作,一般都是没有问题的,编译器可以准确地识别出,哪些操作可以重排序,而不会影响到逻辑
  • 多线程下,判定就可能不准确了,可能出现重排序后,逻辑发生了改变

对于instance = new SingletonLazy();可以大体上细分为三个步骤:

  1. 申请内存空间
  2. 调用构造方法(对内存空间进行初始化)
  3. 把此时内存空间的地址,赋值给instance引用

在指令重排序优化策略下,上述执行的过程,不一定是123,有可能是132(1一定是先执行的),这两种执行方式,单线程下都是可以的,但是如果是132,在多线程下,可能会引起bug

package thread;class SingletonLazy {private static SingletonLazy instance = null;private static Object locker = new Object();public static SingletonLazy getInstance(){if (instance == null){synchronized (locker){if (instance == null){instance = new SingletonLazy();}}}return instance;}private SingletonLazy() {}
}
public class Demo28 {public static void main(String[] args) {SingletonLazy s1 = SingletonLazy.getInstance();SingletonLazy s2 = SingletonLazy.getInstance();System.out.println(s1 == s2);}}	
  • t1线程判断instance == null成立,进行加锁,进一步判断instance == null成立,进行instance = new SingletonLazy(),在这一过程中完成了1申请内存和3把地址赋值给引用,一旦3执行完,意味着instance为非null,但是指向的对象其实是一个未初始化的对象(里面的成员都是默认值)
  • 此时t2线程判断instance == null不成立,直接返回instance这个未初始化完毕的对象
  • 然后接下来t1线程才开始进行2调用构造函数

这种情况下,后续的对SingletonLazy s1 = SingletonLazy.getInstance();操作,都是针对未初始化的对象进行操作,存在严重问题

要解决上述问题,就需要引入volatile

  • volatile不仅仅能解决内存可见性问题,也能禁止针对这个变量读写操作的指令重排序问题
  • 指令重排序在很多地方都可能发生,volatile特指的是针对某个对象的读写操作过程中,不会出现重排序
  • 按照加上volatile之后,此时t2线程读到的数据,一定是t1已经构造完毕的完整对象了

上述谈到的指令重排序涉及到的问题很难进行验证,本身就是一个小概率的事件,即使不加volatile运行程序,运行几百次几千次,应该也是正确的,指不定啥时候会出现问题,加上volatile总是万无一失的做法,程序员也不确定是否在某个JVM这样版本中更好的处理这样的问题

面试中考察单例模式

  1. 先写最初的版本,即不考虑线程安全的版本
  2. 加上锁
  3. 加上双重if
  4. 最后加上volatile

关于单例模式的延伸

(1)单例模式要确保反射下安全,即使动用反射也无法破坏单例特性

(2)单例模式要确保序列化下安全,即使动用Java标准库的序列化机制,也无法破坏单例特性

阻塞队列

之前学习过的普通队列和优先级队列都是线程不安全的,阻塞队列是先进先出的、线程安全的并且带有阻塞功能

  • 队列为空,尝试出队列,出队列操作就会阻塞,一直阻塞到队列不为空为止
  • 队列为满,尝试入队列,入队列操作也会阻塞,一直阻塞到队列不满为止

BlockingQueue就是标准库提供的阻塞队列

除了阻塞队列之外,还有消息队列:不是普通的先进先出,而是通过topic这样的参数来对数据进行归类,出队列的时候,指定topic,每个topic下的数据是先进先出的,消息队列往往也会带有阻塞特性

由于消息队列这样的数据结构太好用了,因此实际开发中,经常会把这样的数据结构封装成单独的服务器程序,单独部署

消息队列能够起到的作用,就是实现“生产者消费者模型”

1. 生产者消费者模型

生产者消费者模型,在开发中主要有两方面的意义:

  • 能够让程序进行解耦合
  • 能够使程序削峰填谷

生产者消费者模型的实现:

  • 需要在一个进程内实现,使用阻塞队列即可
  • 需要在分布式系统中实现,需要使用单独部署的消息队列服务器

简单来说生产者消费者模型就是一些线程负责“生产产品”,另一些线程负责“消费产品”

如果“生产产品”速度较慢,那么“消费产品”就会阻塞等待

如果“消费产品”速度较慢,那么“生产产品”就会阻塞等待

也就是说生产者和消费者之间多了一个消息队列

1.1 生产者消费者模型解耦合

在这里插入图片描述

如果让A直接调用B,意味着A的代码中就要包含很多和B相关的逻辑,B的代码中也会包含和A相关的逻辑,彼此之间就有一定的耦合

  • 一旦A做出了修改,可能就会影响到B,反之亦然
  • 一旦A出现了BUG,也容易把B牵连到,反之亦然

在这里插入图片描述

然而在引入了消息队列之后:

  • 站在A的视角,不知道B的存在,只关心和队列的交互
  • 站在B的视角,不知道A的存在,只关心和队列的交互
  • 此时,对A的修改就不太容易影响到B,A如果挂了,也不会影响到B,反之亦然
  • 未来如果再引入C,也让A访问C,A不需要修改任何代码,直接让C从队列里读取数据即可,提升了程序的可扩展能力
1.2 生产者消费者模型削峰填谷

客户端发来的请求,个数多少,没办法提前预知,遇到某些突发情况,就可能会导致客户端给服务的请求激增

在这里插入图片描述

正常情况下,A收到一个客户端的请求,就同样要请求一次B,A收到的请求激增了,B的请求也会激增,但是由于A做的工作比较简单,消耗的资源少,B做的工作更复杂,消耗的资源多,一旦请求量大了,B就容易挂,所以引入消息队列

  • 无论A给队列写的多快,B都可以按照固有的节奏来消费数据
  • B的节奏,就不一定完全跟着A了,相当于队列把B保护起来了
  • B要进行很多重量级操作,比如操作数据库之类的,要消耗很多系统资源花费一定的时间
  • 消息队列没有什么业务逻辑,消耗的硬件资源少,本身就抗造,同时,实际开发中,部署消息队列的机器一般都会给配置比较高的机器/集群

引入消息队列来实现生产者消费者模型,效率是不如直接访问来得更快的,多了一次周转,也多了一次网络通信

2. 生产者消费者代码

package thread;import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;public class Demo29 {public static void main(String[] args) throws InterruptedException {BlockingQueue<String> queue = new ArrayBlockingQueue<>(100);queue.put("A");String elem = queue.take();System.out.println("elem = " + elem);elem = queue.take();System.out.println("elem = " + elem);}
}
package thread;import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;public class Demo30 {public static void main(String[] args) {BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(1000);Thread t1 = new Thread(() ->{try {while (true){Integer value = queue.take();System.out.println("t1 消费:" + value);Thread.sleep(1000);}}catch (InterruptedException e){e.printStackTrace();}});Thread t2 = new Thread(() ->{try {int count = 1;while (true){queue.put(count);System.out.println("t2 生产:" + count);count++;}}catch (InterruptedException e){e.printStackTrace();}});t1.start();t2.start();}
}

3. 阻塞队列实现

package thread;class MyBlockingQueue {private String[] elems = null;//[head, tail)//head位置指向的是第一个元素,tail指向的是最后一个元素的下一个元素private volatile int head = 0;private volatile int tail = 0;private volatile int size = 0;public MyBlockingQueue(int capacity){elems = new String[capacity];}void put(String elem) throws InterruptedException {synchronized (this) {while (size >= elems.length){//队列满了,进行队列阻塞this.wait();}//把新的元素放到tail所在的位置上elems[tail] = elem;tail++;if (tail >= elems.length) {//到达末尾,就回到开头tail = 0;}//更新size的值size++;//唤醒下面 take 阻塞的waitthis.notify();}}String take() throws InterruptedException {synchronized (this) {while (size == 0) {//队列空了,进行阻塞this.wait();}//取出 head 指向的元素String result = elems[head];head++;if (head >= elems.length) {head = 0;}size--;//take 成功一个元素,就唤醒上面put中的wait操作this.notify();return result;}}
}public class Demo31 {public static void main(String[] args) {MyBlockingQueue queue = new MyBlockingQueue(1000);Thread t1 = new Thread(() -> {try {int count = 1;while (true) {queue.put(count + "");System.out.println("生产" + count);count++;}} catch (InterruptedException e) {e.printStackTrace();}});Thread t2 = new Thread(() -> {try {while (true){String result = queue.take();System.out.println("消费" + result);Thread.sleep(1000);}} catch (InterruptedException e) {e.printStackTrace();}});t1.start();t2.start();}
}

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

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

相关文章

SQL Server Management Studio 显示行号

前言 在使用 SQL Server Management Studio (SSMS) 进行数据库管理和查询时&#xff0c;能够看到代码的行号是非常有用的。这可以帮助您更容易地定位代码错误、讨论特定的代码行&#xff0c;或者在执行长查询时快速找到特定行。在本文中&#xff0c;我将向您展示如何在 SSMS 中…

2024年华中杯数学建模竞赛ABC题思路分析

简单分析一下各个题目可能需要用到的方法和模型&#xff0c;完整代码和成品论文见文末 A题 太阳能路灯光伏板的朝向设计问题: 1. 球面几何、天文学相关知识,如赤纬角、太阳高度角、时角等概念和公式 2. 太阳辐射模型,根据太阳能辐射强度、大气衰减系数等计算地表太阳辐射强度…

蓝桥杯第十五届javab组个人总结

javab组 额今天早上打完了得对自己此次比赛做总结&#xff0c;无论是明年还参赛还是研究生蓝桥杯&#xff0c;体验感有点差&#xff0c;第一题其实一开始想手算但怕进位导致不准确还是让代码跑了&#xff0c;但跑第202420242024个数&#xff08;被20和24整除&#xff09;一直把…

【网络编程】Web服务器shttpd源码剖析——线程池调度

hello &#xff01;大家好呀&#xff01; 欢迎大家来到我的网络编程系列之web服务器shttpd源码剖析——线程池调度&#xff0c;在这篇文章中&#xff0c;你将会学习到在Linux内核中如何创建一个自己的并发服务器shttpd&#xff0c;并且我会给出源码进行剖析&#xff0c;以及手绘…

FebHost:注册.CA域名的企业有什么限制?

在加拿大&#xff0c;只要满足加拿大互联网注册管理局的“加拿大注册要求”&#xff0c;任何类型的企业都可以注册.CA域名。这些要求的目的是为了确保.CA域名空间作为一个重要的公共资源得到合理的使用和开发&#xff0c;以促进所有加拿大人的社会和经济发展。 以下是一些主要…

docker安装EelasticSearch、目录权限修改、并安装IK 中文分词器

文章目录 docker安装EelasticSearch、目录权限修改、并安装IK 中文分词器1、docker安装ES2、docker ps发现容器没有正常启动&#xff0c;docker logs 容器id 查看日志发现是挂载目录的权限不足3、修改目录的权限4、使用docker restart 容器id重新启动刚才没有启动成功的容器5、…

Leetcode 4.18

Leetcode 1.无重复字符的最长子串2.最长回文子串3.整数反转4.字符串转换整数 (atoi)5.正则表达式匹配 1.无重复字符的最长子串 无重复字符的最长子串 滑动窗口&#xff0c;先让右指针右移&#xff0c;如果发现这个子串有元素和右指针当前元素重复。 则&#xff1a; 左指针右移…

【嵌入式之中断】

Cortex-M4集成了嵌套式矢量型中断控制器(Nested Vectored Interrupt Controller (NVIC))来实现高效的异常和中断处理。NVIC实现了低延迟的异常和中断处理&#xff0c;以及电源管理控制。它和内核是紧密耦合的。 凡是打断程序顺序执行的事件都称为异常&#xff08;exception&am…

极狐GitLab x LigaAI,AI 时代研发提效新范式

GitLab 是一个全球知名的一体化 DevOps 平台&#xff0c;很多人都通过私有化部署 GitLab 来进行源代码托管。极狐GitLab 是 GitLab 在中国的发行版&#xff0c;专门为中国程序员服务。可以一键式部署极狐GitLab。 近日&#xff0c;极狐GitLab 和 LigaAI 宣布合作&#xff0c;双…

CentOS 7安装Zookeeper

说明&#xff1a;本文介绍如何在CentOS 7操作系统下使用Zookeeper 下载安装 首先&#xff0c;去官网下载所需要安装的版本&#xff0c;我这里下载3.4.9版本&#xff1b; 上传到云服务器上&#xff0c;解压 tar -xvf zookeeper-3.4.9.tar.gz修改配置 进入Zookeeper目录下的co…

【技术变现之道】如何打造IT行业的超级个体?

前言 在当今的数字化时代&#xff0c;IT行业蓬勃发展&#xff0c;为具备技术专长的个人提供了无限的可能性。想要成为IT行业的超级个体&#xff0c;实现知识与技能的变现吗&#xff1f;以下是一些高效途径&#xff0c;助你一臂之力&#xff01; 1. 独立接单外包 1&#xff09…

vue3数字滚动组件

效果图 一、安装插件 npm i vue3-count-to 二、components文件夹下新建BaseCountTo.vue文件 <template><BaseCountTo :endVal"endVal" :decimals"decimals" /> </template> <script setup > import { defineComponent, watch, r…

改手机IP地址的软件推荐

随着移动互联网的普及&#xff0c;手机已成为人们日常生活中不可或缺的一部分。而在使用手机的过程中&#xff0c;IP地址作为一个重要的网络标识&#xff0c;有时也需要进行修改或更改。为了满足这一需求&#xff0c;市面上涌现出了许多改手机IP地址的软件。虎观代理将对这些软…

韩顺平Java | C27 正则表达式

入门介绍 需求&#xff1a;提取文本中某类字符 传统方法&#xff1a;遍历每个字符&#xff0c;判断其是否在ASCII码中某种类型得编码范围内&#xff0c;代码量大&#xff0c;效率不高 正则表达式(RegExp, regular expression)&#xff1a;处理文本的利器&#xff0c;是对字符…

java混淆的公司有哪些

一些提供 Java 混淆服务的公司包括&#xff1a; PreEmptive Solutions&#xff1a;PreEmptive Solutions 提供了一系列用于保护 Java 和 .NET 应用程序的工具&#xff0c;包括混淆、代码压缩、加密和漏洞检测等功能。 DexGuard&#xff1a;DexGuard 是 Guardsquare 公司推出的…

【JavaWeb】异步请求——AJAX

目录 Ajax&#xff08;Asynchronous JavaScript and XML&#xff09;优点传统Web与Ajax的差异Ajax工作流程Ajax 经典应用场景XMLHttpRequest常用方法事件常用属性 ajax: GET请求和POST请求的区别 传统Ajax实现传统方式实现Ajax的不足 $.ajax()语法常用属性参数常用函数参数 Aja…

golang 迷宫回溯算法(递归)

// Author sunwenbo // 2024/4/14 20:13 package mainimport "fmt"// 编程一个函数&#xff0c;完成老鼠找出路 // myMap *[8][7]int 地图&#xff0c;保证是同一个地图&#xff0c;因此是引用类型 // i,j表示对地图的哪个点进行测试 func SetWay(myMap *[8][7]int, …

网络基础-基于TCP协议的Socket通讯

一、Socket通讯基于TCP协议流程图 UDP 的 Socket 编程相对简单些不在介绍。 二、 服务端程序启动 服务端程序要先跑起来&#xff0c;然后等待客户端的连接和数据。 服务端程序首先调用 socket() 函数&#xff0c;创建网络协议为 IPv4&#xff0c;以及传输协议为 TCP 的…

基于XML配置bean(二)

文章目录 1.工厂中获取bean1.静态工厂1.MyStaticFactory.java2.beans.xml3.测试 2.实例工厂1.MyInstanceFactory.java2.beans.xml3.测试 3.FactoryBean&#xff08;重点&#xff09;1.MyFactoryBean.java2.beans.xml3.测试 2.bean配置信息重用继承抽象bean1.beans.xml2.测试 3.…

HarmonyOS实战开发-如何实现一个简单的健康生活应用

功能概述 成就页面展示用户可以获取的所有勋章&#xff0c;当用户满足一定的条件时&#xff0c;将点亮本页面对应的勋章&#xff0c;没有得到的成就勋章处于熄灭状态。共有六种勋章&#xff0c;当用户连续完成任务打卡3天、7天、30天、50天、73天、99天时&#xff0c;可以获得…