【Java ee初阶】多线程(5)

一、wait 和 notify

wait notify 是两个用来协调线程执行顺序的关键字,用来避免“线程饿死”的情况。

wait  和 notify 其实都是 Object 这个类的方法,而 Object这个类是所有类的“祖宗类”,也就是说明,任何一个类,都可以调用wait 和notify这两个方法。

Java标准库中,涉及到阻塞的方法,都可能抛出InterruptedException

让我们来观察一下这个异常的名字。

Illegal:非法的,不正确的,不合理的(而不是违反法律的)、

Monitor:监视器/显示器(电脑的显示器,英文不是screen,而是Monitor)此处的Monitor指的是synchronized,synchronized在JVM里面的底层实现,就被称为“监视器锁”(JVM源码,变量名是Monitor相关的词)

所以这个异常的意思是,当前处于非法的锁状态。

众所周知,锁一共有两种状态,一种是加锁,一种是解锁。

wait方法内部做的第一件事情,就是释放锁。

而我们必须要先得到锁,才能去谈释放锁。因此,wait必须放到synchronized代码块内部去进行使用。

此处的阻塞会持续进行,直到其他线程调用notify把该线程进行唤醒。

此处的阻塞会持续进行,直到其他线程调用notify,将该线程进行唤醒。

package Thread;import java.util.Scanner;public class demo28 {// 将 object 变量移到类内部并添加 static 修饰符public static Object object = new Object();public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {synchronized (object) { System.out.println("t1 wait之前");try {object.wait(); } catch (InterruptedException e) {e.printStackTrace();}System.out.println("t1 wait之后"); }});Thread t2 = new Thread(() -> {Scanner scanner = new Scanner(System.in);System.out.println("请输入任意内容 尝试唤醒t1");scanner.next(); synchronized (object) { object.notify(); System.out.println("t2 notify之后"); }});     t1.start();t2.start();}   
}

输出:

图上这四处地方的锁,必须是同一个对象。

假设notify后面又有一堆别的逻辑,此时,这个锁就会再多占有一会。

【总结】wait要做的事情:

1、使当前执行代码的线程进行等待(把线程放到等待队列中去)

2、释放当前的锁

3、满足一定条件的时候被唤醒,并且重新尝试获取这把锁

(这三个步骤是同时进行的)

使用wait的时候,阻塞其实是有两个阶段的:

1、WAITING的阻塞:通过wait 等待其他线程的通知

2、BLOCKED阻塞:当收到通知之后,就会重新尝试获取这把锁。重新尝试获取这把锁,很可能又会遇到锁竞争

wait进行阻塞之后,需要通过notify唤醒。默认情况下,wait的阻塞也是死等。

这样子是不合理的,因此,我们在工作中需要设定等待时间上限。(超过时间)

括号里的等待时间是毫秒

package Thread;import java.util.Scanner;public class demo29 {public static Object locker = new Object();public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {synchronized (locker) {System.out.println("t1 wait之前");try {locker.wait();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("t1 wait之后");}});Thread t2 = new Thread(() -> {synchronized (locker) {System.out.println("t2 wait之前");try {locker.wait();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("t2 wait之后");}});Thread t3 = new Thread(() -> {synchronized (locker) {System.out.println("t3 wait之前");try {locker.wait();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("t3 wait之后");}});Thread t4 = new Thread(() -> {Scanner scanner = new Scanner(System.in);System.out.println("请输入任意内容 尝试唤醒t1、t2、t3");scanner.next();synchronized (locker) {System.out.println("t4 notify之前");locker.notify(); // 唤醒一个在 locker 上等待的线程,这里是 t1System.out.println("t4 notify之后");}});t1.start();t2.start();t3.start();t4.start();}}

输出:

可以看出,当前只是将t1唤醒了

再次尝试

唤醒的仍然是t1

咱们在多线程中谈到的“随机”其实不是数学上概率均等的随机,这种随机的概率是无法预测的。取决于调度器如何去调度。调度器里面,其实不是“概率均等的唤醒”,调度器内部也是有一套规则的。这套规则,对于程序员是“透明的”,程序员做的,就是不能依赖于这里的状态。

mysql的时候,select查询一个数据,得到的结果集,是按照怎样的顺序的呢?(是按照id的顺序,时间的顺序,排列的顺序的吗?)都不是,mysql就没有这样的承诺。必须加上orderby

notifyAll可以唤醒全部:

如果没有任何对象在wait,那么直接调用notify / notifyAll 会发生什么?
不会发生任何事情,直接凭空调用notify是没有任何副作用的

经典面试题:

请你谈一谈sleep  和 wait 的区别

1.wait 的设计就是为了提前唤醒。超时时间,是“后手”(B计划)

sleep 的设计就是为了到达时间再进行唤醒。虽然也可以通过Interrupt()进行提前唤醒,但是这样的唤醒是会产生异常的。(此处的异常表示:程序出现不符合预期的情况,才称为“异常”)

2.wait需要搭配锁来时进行使用,wait执行时会先释放锁

 sleep不需要搭配锁进行使用,当把sleep放到synchronized内部的时候,不会释放锁(抱着锁睡觉)

综上所述,在实际开发中,wait比sleep用的更多。

二、单例模式

单例模式是一种设计模式,校招中最常考到的设计模式之一。

为了使得新手的代码下线也能够有所保证,大佬们研究出了一些“设计模式”,用来解决一些固定的场景问题,这些问题有着固定的套路。

如果按照设计模式写,能够得到一个较为靠谱的代码,属于是一种软性要求。

设计模式有很多很多种类,不仅仅有23种。

单例模式 :单例,也就是单个实例(单个对象)。虽然一个类,在语法角度来说,是可以无限创建实例的,但是在实际的场景当中,可能有时候我们只希望这个类只有一个实例(例如JDBC)

那么,在Java代码中,如何实现单例模式呢?——有很多种实现方式,其中最主要的有两种模式:

1、饿汉模式

创建实例的时机是非常紧迫的。

由于此处的Instance是一个static 成员,创建时机,就是在类加载的时候。也就是说,程序一启动,实例就被创建好了。

package Thread;class Singleton{private static Singleton instance = new Singleton();public static Singleton getInstance(){return instance;}   //做了一个“君子协定”,让其他类不能new这个类,只能通过getInstance()方法获取这个类的实例。private Singleton(){}
}public class demo33 {public static void main(String[] args) {Singleton instance1 = Singleton.getInstance();Singleton instance2 = Singleton.getInstance();System.out.println(instance1 == instance2);}}

  做了一个“君子协定”,让其他类不能new这个类,只能通过getInstance()方法获取这个类的实例。

2、懒汉模式

第一次使用这个实例的时候,才会创建这个实例,创建的时机更晚

上述两份代码,哪一份是线程安全的,哪一份是线程不安全的呢?

而懒汉模式容易因此下述问题:

最终只创建了一个实例!

实际开发中,单例类的构造方法可能是一个非常重量的方法。我们之前,代码中也有单例模式的使用。当时通过单例类,管理整个服务器程序所以来的所有数据(100G)。这个实例创建的过程,就会从硬盘上把100G的数据加载到内存当中。

那么,如何解决该问题呢?

我们可以通过加锁,将操作打包成原子的来解决该问题。

但是,这串代码仍然存在问题:逻辑上来看,我们只是在第一次调用的时候,才会涉及到线程安全问题,只要对象创建完毕,后序都是直接return了,就不涉及修改了。但是,此时这个代码,锁是每次调用都会加上的。明明已经线程阿耐庵了,但是还要再进行加锁,这并不合理。

图中,一摸一样的条件连续写了两遍。以前都是一个线程,这个代码执行下来,第一次判定和第二次判定,结论是一定相同的。

而现在是多线程,第一次判定和第二次判定,结论可以不一样。因为再第一次和第二次判定之间,可能有另外一个线程,修改了instance。多线程,打破了以前的认知。而后面学习的网络,EE进阶里面的框架,也会打破以前的认知。

在多线程当中,指令重排序容易引起线程安全问题。指令重排序是编译器优化的一种手段,这是编译器在确保逻辑一致的情况下,为了提高效率,调整代码的顺序,就可以让效率变高了。然而,指令重排序在遇见多线程就又出现问题了。

此处涉及到的指令是非常多的,为了简化这个模型,我们将他抽象成三个步骤:

1、申请内存空间

2、在内存空间上进行初始化(构造方法)

3、内存地址,保存到引用变量当中

在多线程中,由于指令重排序,容易引起上述问题。

Instance里面还没有任何的属性方法,但是已经被线程2拿去使用了!

那么,如何避免指令重排序的问题呢?

只需要加上volatile这个关键字。

volatile的意思是,针对这个变量的读写操作,不要触发优化。

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

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

相关文章

基于k8s的Jenkins CI/CD平台部署实践(二):流水线构建与自动部署全流程

基于k8s的Jenkins CI/CD平台部署实践(二):流水线构建与自动部署全流程 文章目录 基于k8s的Jenkins CI/CD平台部署实践(二):流水线构建与自动部署全流程一、Jenkins简介二、系统架构与环境说明1. 系统架构2.…

《Windows 环境下 Qt C++ 项目升级 GCC 版本的完整指南》

Windows 环境下 Qt C++ 项目升级 GCC 版本的完整指南 在 Windows 系统中升级 Qt C++ 项目的 GCC 版本需要同时考虑 Qt 工具链、MinGW 环境以及项目配置的调整。以下是详细的升级步骤和注意事项: 一、升级前的准备工作 1. 确认当前环境 检查 Qt 版本(建议使用 Qt 5.15+ 以获…

【coze】故事卡片(图片、音频、文字)

【coze】故事卡片(图片、音频、文字) 1、创建智能体2、添加人设与回复逻辑3、添加工作流(1)创建工作流(2)添加大模型节点(3)添加提示词优化节点(4)添加豆包图…

Maven 依赖发布与仓库治理

🧑 博主简介:CSDN博客专家,历代文学网(PC端可以访问:https://literature.sinhy.com/#/?__c1000,移动端可微信小程序搜索“历代文学”)总架构师,15年工作经验,精通Java编…

虚拟现实视频播放器 2.6.1 | 支持多种VR格式,提供沉浸式观看体验的媒体播放器

虚拟现实媒体播放器是一款专为在智能手机上播放VR(虚拟现实)照片和视频而设计的应用程序。它支持多种格式的影像内容,包括360和180等距矩形柱面、标准镜头和鱼眼镜头拍摄的照片和视频,并且兼容3D立体并排、上/下以及收缩媒体格式。…

ts axios中报 Property ‘code‘ does not exist on type ‘AxiosResponse<any, any>‘

ts语法有严格的格式,如果我们在处理响应数据时,出现了axios响应中非默认字段,就会出现标题那样的警告,我们可以通过创建axios.dt.ts解决这个问题 下面是我在开发中遇到的警告,code并不是axios默认返回的字段&#xff0…

tinyrenderer笔记(Shadow Mapping)

tinyrenderer个人代码仓库:tinyrenderer个人练习代码 前言 阴影是光线被阻挡的结果;当光源的光线由于其他物体的阻挡而无法到达物体表面时,该物体就会产生阴影。阴影能使场景看起来更真实,并让观察者获得物体之间的空间位置关系。…

debian中笔记本的省电选择auto-cpufreq

在reddit中,看评论区出现这个软件,于是打算尝试一下,应该能对不使用电源时笔记本的省电起到一定的作用。 https://github.com/AdnanHodzic/auto-cpufreq?tabreadme-ov-file#why-do-i-need-auto-cpufreq 作用 One of the problems with Linux…

Windows 查看电脑是否插拔过U盘

1、按 “WinR” 组合键打开 “运行” 对话框,输入 “regedit” 并回车,打开注册表编辑器。 2、依次展开HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Enum\USBSTOR注册表项,这里记录了所有已连接过的 USB 设备信息,包括 U 盘&am…

426、N叉树的层序遍历

输入检查: if not root:return [] 如果根节点为空,直接返回空列表 初始化: result [] queue collections.deque([root]) result用于存储最终结果queue初始化包含根节点,使用双端队列实现 主循环: while queue:leve…

【ES】Elasticsearch字段映射冲突问题分析与解决

在使用Elasticsearch作为搜索引擎时,经常会遇到一些映射(Mapping)相关的问题。本文将深入分析字段映射冲突问题,并通过原生的Elasticsearch API请求来复现和解决这个问题。 问题描述 在实际项目中,我们遇到以下错误: Transport…

小红书怎么看自己ip地址?小红书更改ip地址教学

在社交媒体高度透明的今天,小红书等平台公开用户IP属地的功能引发了广泛讨论。无论是出于隐私保护的担忧,还是因需要切换属地,许多用户都迫切想知道:能否通过手动修改“伪装”所在地? 事实上,IP属地可能影…

深入理解 Java 观察者模式:原理、实现与应用

在软件开发领域,设计模式堪称开发者智慧的凝练结晶,它们为解决各类常见编程难题提供了行之有效的方案。观察者模式(Observer Pattern)作为行为型设计模式的重要一员,在处理对象间依赖关系与事件通知方面表现卓越。本文…

网络原理 TCP/IP

1.应用层 1.1自定义协议 客户端和服务器之间往往进行交互的是“结构化”数据,网络传输的数据是“字符串”“二进制bit流”,约定协议的过程就是把结构化”数据转成“字符串”或“二进制bit流”的过程. 序列化:把结构化”数据转成“字符串”…

2025年5月HCIP题库(带解析)

某个ACL规则如下:则下列哪些IP地址可以被permit规则匹配: rule 5 permit ip source 10.0.2.0 0.0.254.255 A、10.0.4.5 B、10.0.5.6 C、10.0.6.7 D、10.0.2.1 试题答案:A;C;D 试题解析: 10.0.2.000001010.00000000.00000010.0000000…

【Redis | 基础总结篇 】

目录 前言: 1.Redis的介绍: 2.Redis的类型与命令: 3.Redis的安装: 3.1.Windows版本 3.2.Linux版本 4.在java中使用Redis: 4.1.介绍 4.2.Jedis 4.3.Spring Data Redis 前言: 本篇主要讲述了Redis的…

38.前端代码拆分

因为前端代码之前是一体编写的,所以为了方便对代码进行了拆分 之前是这样的: 为了更加规范,所以拆分成vue、util、store、api等部分: css: store: 拆分后的大致界面为: 其实还有点别扭需要后续再调整

tinyrenderer笔记(Shader)

tinyrenderer个人代码仓库:tinyrenderer个人练习代码 前言 现在我们将所有的渲染代码都放在了 main.cpp 中,然而在 OpenGL 渲染管线中,渲染的核心逻辑是位于 shader 中的,下面是 OpenGL 的渲染管线: 蓝色是我们可以自…

C++高性能内存池

目录 1. 项目介绍 1. 这个项目做的是什么? 2. 该项目要求的知识储备 2. 什么是内存池 1. 池化技术 2. 内存池 3. 内存池主要解决的问题 4.malloc 3. 先设计一个定长的内存池 4.高并发内存池 -- 整体框架设计 5. 高并发内存池 -- thread cache 6. 高并发内存池 -- …

LintCode407-加一,LintCode第479题-数组第二大数

第407题: 描述 给定一个非负数,表示一个数字数组,在该数的基础上1,返回一个新的数组。 该数字按照数位高低进行排列,最高位的数在列表的最前面. 样例 1: 输入:[1,2,3] 输出:[1,2,4] 样例 …