初始JavaEE篇——多线程(4):wait、notify,饿汉模式,懒汉模式,指令重排序

找往期文章包括但不限于本期文章中不懂的知识点:
个人主页:我要学编程(ಥ_ಥ)-CSDN博客
所属专栏:JavaEE
目录
wait、notify 方法
多线程练习
单例模式
饿汉模式
懒汉模式
指令重排序
wait、notify 方法
wait 和 我们前面学习的sleep、join方法一样,也是让线程阻塞,但是其可以被notify方法唤醒,但是sleep是被Interrupt给提前唤醒或者指定时间过了之后自动被唤醒,并且会抛出异常。且 join 是一个线程等待另一个线程,并且要 被等待的线程彻底执行完成之后,等待的线程才会从阻塞的中被唤醒重新执行。
wait方法在使用时,要和synchronized一起搭配使用,因为其是先对调用它的对象进行解锁,阻塞,在被唤醒之后,在进行加锁操作。
public class Test {public static void main(String[] args) throws InterruptedException {Object locker1 = new Object();Thread t = new Thread(()->{System.out.println("wait之前");synchronized (locker1) { // 加锁try {locker1.wait(); // 进入wait方法解锁,出wait方法加锁} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("wait之后");});t.start();// 为了让t线程先wait阻塞等待,得先休眠主线程一会:// 可以使用sleep方法,也可以使用IO的方法阻塞Thread.sleep(1000);System.out.println("输入任意内容,唤醒t线程");Scanner scanner = new Scanner(System.in);scanner.next();// 要出wait方法就需要notify进行唤醒操作synchronized (locker1) {locker1.notify();}}
}
注意:
1、在Java中,wait 和 notify 方法一定是和 synchronized 一起使用的。
2、在1的基础上,四者的进行加锁解锁的操作一定是针对同一个锁对象。
3、notify 的唤醒操作一定是在 wait 之前才能有效的唤醒。如果先执行了 notify 的唤醒操作,但是 还没有执行wait的阻塞操作的话,那么线程就一直会阻塞,但是 notify 的唤醒操作对线程本身是不会有影响的。
4、wait 和 notify 方法是 Object 对象的方法,即所有对象都可以使用这两个方法。
5、如果有多个线程处于 wait 的阻塞状态,那么 notify 一次只能随机唤醒一个线程。如果想要全部唤醒的话,得使用 notifyAll 方法。当然,也可以使用 notifyAll 去唤醒一个线程。
6、wait 和 join一样,也提供了最大等待时间。当超出这个最大等待时间时,被 wait 方法阻塞的线程将不会在处于阻塞状态。
public class Test {public static void main(String[] args) throws InterruptedException {Object locker = new Object();Thread t = new Thread(()->{System.out.println("wait之前");synchronized (locker) {try {locker.wait(5000);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("wait之后");System.out.println("t线程结束");});t.start();Thread.sleep(1000);System.out.println("输入任意内容,唤醒t线程");Scanner scanner = new Scanner(System.in);scanner.next();synchronized (locker) {locker.notify();}}
}
当我们迟迟没有去输入值时,如果已经超过了 wait 的最大阻塞时间的话, wait 便不会去阻塞 t 线程了,而是会让其继续执行下去,即使我们后续再次输入值来执行 notify 的唤醒操作,也不再有用了。

7、当一个线程执行到 wait 之后,这个锁被释放了,也就意味着有别的线程可以使用这把锁了。
多线程练习
到此为止,我们已经学习了不少的多线程知识,现在我们就来练习一下。
题目:
有三个线程:t1、t2、t3,三者分别打印A、B、C,现在我们需要打印10次ABC。
思路:
1、既然打印有先后顺序,那么我们肯定是可以通过手动控制sleep 的休眠时间来决定的。
2、刚刚我们学习了 wait 和 notify ,应该是可以想到这个应用场景的,完全对上了。一个 打印完A之后,唤醒另一个线程,打印B,接着唤醒另一个线程打印C,最后 t3线程唤醒 t1线程,就这样相互唤醒打印,而 main 线程用来唤醒 t1 线程开始最初的打印即可。
代码实现:
1、暴力-sleep:
public class Test {public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(()->{for (int i = 0; i < 10; i++) {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.print("A");}});Thread t2 = new Thread(()->{for (int i = 0; i < 10; i++) {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.print("B");}});Thread t3 = new Thread(()->{for (int i = 0; i < 10; i++) {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("C");}});t1.start();Thread.sleep(10); // 确保 t1是最先执行的t2.start();Thread.sleep(10); // 确保 t2比t1后执行,比t3先执行t3.start();}
}
注意:这里使三个线程的执行顺序的确定,其休眠的时间不能过长,否则不好衔接。
2、wait-notify版本:
public class Test {public static void main(String[] args) throws InterruptedException {Object locker1 = new Object();Object locker2 = new Object();Object locker3 = new Object();Thread t1 = new Thread(()->{try {for (int i = 0; i < 10; i++) {synchronized (locker1) {locker1.wait();}System.out.print("A");synchronized (locker2) { // 要清楚唤醒的是哪个线程locker2.notify();}}} catch (InterruptedException e) {throw new RuntimeException(e);}});Thread t2 = new Thread(()->{try {for (int i = 0; i < 10; i++) {synchronized (locker2) {locker2.wait();}System.out.print("B");synchronized (locker3) { // 要清楚唤醒的是哪个线程locker3.notify();}}} catch (InterruptedException e) {throw new RuntimeException(e);}});Thread t3 = new Thread(()->{try {for (int i = 0; i < 10; i++) {synchronized (locker3) {locker3.wait();}System.out.println("C");synchronized (locker1) { // 要清楚唤醒的是哪个线程locker1.notify();}}} catch (InterruptedException e) {throw new RuntimeException(e);}});t1.start();t2.start();t3.start();// 确保t1先执行到了waitThread.sleep(1000);synchronized (locker1) { // notify一定要和synchronized配合使用locker1.notify();}}
}
单例模式
单例模式属于设计模式的一种,是指一个进程中,一个类只能实例化一个对象,即单个实例。那怎么去实现一个进程中只能有一个对象呢?直接把构造方法改为private即可,这样在外部就不能创建实例了。
单例模式中,最常见的就是饿汉模式与懒汉模式。
饿汉模式
饿汉模式,主要体现在"饿"字上,因为其是迫不及待的去创建类的实例。
代码演示:
// 饿汉模式
class SingleTon {// 迫不及待的创建实例private static SingleTon singleTon = new SingleTon();public static SingleTon getInstance() {return singleTon;}// 单例模式的构造方法一定是private修饰的private SingleTon() {}
}
这里创建类的实例是通过创建一个静态的成员变量来实现的,而静态的成员变量是类在加载时,就会被创建,即JVM中有这个类存在的痕迹的话,那么这个实例就会存在。 因此,以"饿"得名。
我们也可以去检查这个饿汉模式是否创建成功,主要检查是否是单例模式。
public class Test {public static void main(String[] args) {// SingleTon s = new SingleTon(); // errorSingleTon s1 = SingleTon.getInstance();SingleTon s2 = SingleTon.getInstance();System.out.println(s1 == s2); // true}
}
从上面的程序运行的结果,可以得知:一个进程中不能实例化多个对象,符合单例模式的特征。
懒汉模式
懒汉模式,主要体现在"懒"字上,只有当迫不得已时,才去创建实例。
代码演示:
// 懒汉模式
class SingleTonLazy {// 迫不得已才创建实例private static SingleTonLazy singleTonLazy = null;public static SingleTonLazy getInstance() {if (singleTonLazy == null) {singleTonLazy = new SingleTonLazy(); // 一定要把对象保留下来}return singleTonLazy;}// 单例模式的构造方法一定是私有的private SingleTonLazy() {}
}
懒汉模式只有当外部调用getInstance方法时,才会去创建实例,否则就不会创建实例。
同样也可以去测试这个懒汉模式是否创建成功。
public class Test {public static void main(String[] args) {// SingleTonLazy s = new SingleTonLazy(); // errorSingleTonLazy s1 = SingleTonLazy.getInstance();SingleTonLazy s2 = SingleTonLazy.getInstance();System.out.println(s1 == s2); // true}
}
上面的懒汉模式在单线程下使用没问题,但是在多线程下使用,便会出现线程安全问题。(饿汉模式之所没有线程安全问题,是因为饿汉模式只是进行return的"读"操作,而不是和懒汉模式一样,有"写"操作)
因为懒汉模式的创建线程虽然只是一个赋值代码,也就是对应一条CPU指令,但是有了 if 语句之后,两者就不算是原子的了。
例如,当线程1去实例化一个对象时,执行到 if 语句,但偏偏此时操作系统将其从CPU上踢下去了,然后线程2就也去CPU上执行了实例化对象的操作,和线程1一样只是执行到 if 语句,也被赶下去了,接着 线程1执行了赋值语句成功的创建了一个对象,然后线程2又被调度到CPU上了,也执行了创建对象的赋值语句。
上面就会导致两个问题:
1、 这里new了两次,即创建了两次对象破坏了单例模式的初衷。
2、后一次new的对象会覆盖前面的对象,可以会对程序的数据造成影响,最终导致程序崩溃。
这里有小伙伴可能会疑惑:为什么线程1创建了对象之后,线程2还会去创建对象呢?因为线程1创建完成之后,线程2已经执行到了 if 语句之中,其认为还没有创建对象。
因此,我们得对上述代码进行加锁操作。
// 懒汉模式
class SingleTonLazy {// 迫不得已才创建实例private static SingleTonLazy singleTonLazy = null;private static Object locker = new Object();public static SingleTonLazy getInstance() {synchronized (locker) {if (singleTonLazy == null) {singleTonLazy = new SingleTonLazy(); // 一定要把对象保留下来}}return singleTonLazy;}// 单例模式的构造方法一定是私有的private SingleTonLazy() {}
}
加锁操作确实可以实现线程安全,但是它也会造成程序的性能下降,因为当对象的实例被创建出来后,别的线程再去调用这个方法时,就会进行加锁操作,而加锁对于最终的结果来说没影响,也就是加锁加了个寂寞,这就是在浪费时间了。因此,也就导致了性能下降了。
我们的解决方法是在锁的最外层再加上一个 if 语句去判断,这样即使有了实例之后,别的线程再尝试去创建实例时,就会直接return,而不会再去进行加锁操作了,这样性能就提升了不少。
public static SingleTonLazy getInstance() {if (singleTonLazy == null) {synchronized (locker) {if (singleTonLazy == null) {singleTonLazy = new SingleTonLazy(); // 一定要把对象保留下来}}}return singleTonLazy;}
指令重排序
上面的懒汉模式代码,还是有点问题,这个问题和指令重排序有关。
概念:指令重排序是指在不影响代码的执行逻辑的基础上,编译器对要执行的代码其底层对应的计算机指令进行了优化处理,会使其与原来的执行顺序不一致。
懒汉模式的指令重排序体现在 赋值语句。我们先来学习一下,这个赋值语句,其底层对应的逻辑:1、向内存申请了一块空间;2、在这块空间内构造对象(初始化成员变量等)3、将这块空间的首地址给到引用变量。如果将上述三个操作类比到我们日常生活的话,那就是1、买房子;2、装修;3、拿到钥匙。
指令重排序可能会使这个1、2、3的顺序打乱,变成1、3、2。虽然这个在日常生活中,即使打乱之后,我们也是不会直接入住的,因为还没有装修,但是计算机可不一样,它是一个铁憨憨,他只知道执行工作,因此当它执行了1、3之后,也就是拿到了这个对象的引用之后,如果此时操作系统将其从CPU上踢下去了,让别的线程来执行相关方法的话,这个操作就不亚于在毛坯房中直接拎包入住的行为了。这可能直接就把程序给搞崩溃了。因此,我们不能让指令重排序的行为发生,这里就需要用到 volatile 关键字了。这个关键字既可以避免 内存可见性的问题,也可以避免指令重排序的问题。
private static volatile SingleTonLazy singleTonLazy = null;
好啦!本期 初始JavaEE篇——多线程(4):wait、notify,饿汉模式,懒汉模式,指令重排序 的学习之旅 就到此结束啦!我们下一期再一起学习吧!
相关文章:
初始JavaEE篇——多线程(4):wait、notify,饿汉模式,懒汉模式,指令重排序
找往期文章包括但不限于本期文章中不懂的知识点: 个人主页:我要学编程(ಥ_ಥ)-CSDN博客 所属专栏:JavaEE 目录 wait、notify 方法 多线程练习 单例模式 饿汉模式 懒汉模式 指令重排序 wait、notify 方法 wait 和 我们前面学习的sleep…...
Apache Solr 身份认证绕过导致任意文件读取漏洞复现(CVE-2024-45216)
0x01 产品简介 Apache Solr是一个开源的搜索平台,基于流行的Apache Lucene库构建。它提供了一个强大的全文搜索功能,能够快速处理大量数据,并支持复杂的搜索操作。并且是一个独立的企业级搜索应用服务器,它采用Java开发,并基于Apache Lucene实现。Solr提供了类似于Web-Se…...
C#整合Ollama实现本地LLMs调用
前言 近两年AIGC发展的非常迅速,从刚开始的只有ChatGPT到现在的很百家争鸣。从开始的大参数模型,再到后来的小参数模型,从一开始单一的文本模型到现在的多模态模型等等。随着一起进步的不仅仅是模型的多样化,还有模型的使用方式。…...
C++基于opencv的视频质量检测--图像抖动检测
文章目录 0.引言1. 原始代码分析2. 优化方案3. 优化后的代码4. 代码详细解读 0.引言 视频质量图像抖动检测已在C基于opencv4的视频质量检测中有所介绍,本文将详细介绍其优化版本。 1. 原始代码分析 首先,我们来看图像抖动检测的原始代码: …...
Cuda By Example - 11 (Texture Memory 2-D)
跟1D一样,2D的代码也没有运行过。旧的方法看看就好。 声明二维Texture texture<float, 2> texConstSrc; texture<float, 2> texIn; texture<float, 2> texOut; 访问二维Texture 使用2D的Texture的便利性体现在blend_kernel函数里。不再需要通…...
Go匿名结构体使用场景
1. 定义 在 Go 语言中,匿名结构体(Anonymous Struct)是一种没有显式命名的结构体类型。你可以直接在代码中定义并使用匿名结构体,而不需要为其定义一个单独的类型名称。匿名结构体通常用于临时数据结构或一次性使用的场景。 匿名…...
Vue 发布十年了!你知道我这十年是怎么过的吗?
2014 年 2 月 3 日,Vue 在 Hacker News 上首次亮相。十年后的今天,Vue 已经成为使用最广泛的前端框架之一,拥有了一个非常丰富的生态系统。本文来梳理一下 Vue.js 十年以来的重要里程碑! 尤雨溪,无疑是 Vue.js 背后的灵…...
Unity 6 来袭
这里写自定义目录标题 1.提升渲染性能1.1 降低CPU开销 Lower CPU overhead1.2.减少内存带宽1.3.高档低分辨率帧2.多人游戏创作3.扩大多平台覆盖范围3.1.增进Android平台开发4.使用Runtime AI解锁各种可能性4.1.Unity Muse4.2.Unity Sentis5.实现更具吸引力的视觉效果5.1.自适应…...
SpringMVC课时1
一:SpringMVC Spring MVC 是 Spring 提供的一个基于 MVC 设计模式的轻量级 Web 开发框架,本质上相当于 Servlet,负责表述层(控制层)实现简化。 由于 Spring MVC 本身就是 Spring 框架的一部分,和 Spring 框架是无缝集成。 二:SSM的主要作用 三:SpringMVC的原理架构图 …...
【小白学机器学习30】样本统计的核心参数:均值/期望,方差,标准差,标准值。
目录 1 为什么我们要搞出来这么多指标/参数? 1.1 描述统计学为啥要搞出来这么多复杂的参数?什么平均值等 1.2 所以,需要用少数几个关键数据代表1群数据 1.2.1 平均值 1.2.2 平均值的问题:方差 2 代表性的数据1:…...
flink1.17.2安装和使用
版本:flink1.17.2 单机模式 配置 # 为了在别处连接flink-web rest.bind-address: 0.0.0.0命令 # 启动集群 bin/start-cluster.sh # 关闭集群 bin/stop-cluster.sh使用 使用浏览器连接 ip:8081 使用flink-web...
C向C++入门-- C语言填坑
1.c参考文档 我们在学习c中需要查找参照信息到是从这些文档中得到。 https://legacy.cplusplus.com/reference/ 标准只更新到C11,但是以头⽂件形式呈现,内容⽐较易看好懂。 https://zh.cppreference.com/w/cpp https://en.cppreference.com/w/ 后两…...
扫雷游戏(C语言详解)
扫雷游戏(C语言详解) 放在最前面的1、前言(扫雷游戏的简介)2、扫雷游戏的规则(简易版)3、代码实现(3.1)提醒一下:( i ) 提醒1:( ii ) 提醒2: &…...
信刻全自动光盘摆渡系统
随着各种数据传输、储存技术、信息技术的快速发展,保护信息安全是重中之重。各安全领域行业对跨网数据交互需求日益迫切。针对于业务需要与保密规范相关要求,涉及重要秘密信息,需做到安全的物理隔离,并且保证跨网数据高效安全传输…...
计算机网络的数据链路层
计算机网络的数据链路层 数据链路层是OSI参考模型中的第二层,它位于物理层之上,网络层之下。数据链路层的主要功能是在物理层提供的服务的基础上向网络层提供服务,其最基本的服务是将源自网络层来的数据可靠地传输到相邻节点的目标机网络层。…...
从0开始搭建一个生产级SpringBoot2.0.X项目(三)SpringBoot接口统一返回和全局异常处理
前言 最近有个想法想整理一个内容比较完整springboot项目初始化Demo。 SpringBoot接口统一返回和全局异常处理,使用ControllerAdvice ExceptionHandler 的组合来实现。 一、pom文件新增依赖 <dependency><groupId>com.alibaba</groupId><ar…...
Mybatis-plus-扩展功能
Mybatis-plus-扩展功能 一:代码生成器 AutoGenerator 是 MyBatis-Plus 的代码生成器,通过 AutoGenerator 可以快速生成 Entity、Mapper、Mapper XML、Service、Controller 等各个模块的代码,极大的提升了开发效率。 功能的演示:…...
【AI辅助】AWS Toolkit+AmazonQ
#偶然看到网上某up主用的这个AI工具,感觉还挺实用的,推荐大家~我们不可阻挡AI的攻势,但是成为利用它的人,也是反侵占的方式呢# AWS toolkit Amazon Q 安装 VScode--Extensions--搜索工具--安装 安装后,工具栏会多出对…...
云手机简述(概况,使用场景,自己部署云手机)
背景 最近经常会看到云手机的相关广告,手痒难耐,了解一下。 我的主要需求: Android 已 root,能够做一些自动化等高级功能。能够通过 远程adb 控制手机。能够尽量的少花钱,最好是能够提供动态创建删除手机的方式&…...
Java已死,大模型才是未来?
作者:不惑_ 引言 在数字技术的浪潮中,编程语言始终扮演着至关重要的角色。Java,自1995年诞生以来,便以其跨平台的特性和丰富的生态系统,成为了全球范围内开发者们最为青睐的编程语言之一 然而,随着技术的…...
k8s从入门到放弃之Ingress七层负载
k8s从入门到放弃之Ingress七层负载 在Kubernetes(简称K8s)中,Ingress是一个API对象,它允许你定义如何从集群外部访问集群内部的服务。Ingress可以提供负载均衡、SSL终结和基于名称的虚拟主机等功能。通过Ingress,你可…...
听写流程自动化实践,轻量级教育辅助
随着智能教育工具的发展,越来越多的传统学习方式正在被数字化、自动化所优化。听写作为语文、英语等学科中重要的基础训练形式,也迎来了更高效的解决方案。 这是一款轻量但功能强大的听写辅助工具。它是基于本地词库与可选在线语音引擎构建,…...
面向无人机海岸带生态系统监测的语义分割基准数据集
描述:海岸带生态系统的监测是维护生态平衡和可持续发展的重要任务。语义分割技术在遥感影像中的应用为海岸带生态系统的精准监测提供了有效手段。然而,目前该领域仍面临一个挑战,即缺乏公开的专门面向海岸带生态系统的语义分割基准数据集。受…...
pikachu靶场通关笔记19 SQL注入02-字符型注入(GET)
目录 一、SQL注入 二、字符型SQL注入 三、字符型注入与数字型注入 四、源码分析 五、渗透实战 1、渗透准备 2、SQL注入探测 (1)输入单引号 (2)万能注入语句 3、获取回显列orderby 4、获取数据库名database 5、获取表名…...
提升移动端网页调试效率:WebDebugX 与常见工具组合实践
在日常移动端开发中,网页调试始终是一个高频但又极具挑战的环节。尤其在面对 iOS 与 Android 的混合技术栈、各种设备差异化行为时,开发者迫切需要一套高效、可靠且跨平台的调试方案。过去,我们或多或少使用过 Chrome DevTools、Remote Debug…...
LCTF液晶可调谐滤波器在多光谱相机捕捉无人机目标检测中的作用
中达瑞和自2005年成立以来,一直在光谱成像领域深度钻研和发展,始终致力于研发高性能、高可靠性的光谱成像相机,为科研院校提供更优的产品和服务。在《低空背景下无人机目标的光谱特征研究及目标检测应用》这篇论文中提到中达瑞和 LCTF 作为多…...
OCR MLLM Evaluation
为什么需要评测体系?——背景与矛盾 能干的事: 看清楚发票、身份证上的字(准确率>90%),速度飞快(眨眼间完成)。干不了的事: 碰到复杂表格(合并单元…...
goreplay
1.github地址 https://github.com/buger/goreplay 2.简单介绍 GoReplay 是一个开源的网络监控工具,可以记录用户的实时流量并将其用于镜像、负载测试、监控和详细分析。 3.出现背景 随着应用程序的增长,测试它所需的工作量也会呈指数级增长。GoRepl…...
Appium下载安装配置保姆教程(图文详解)
目录 一、Appium软件介绍 1.特点 2.工作原理 3.应用场景 二、环境准备 安装 Node.js 安装 Appium 安装 JDK 安装 Android SDK 安装Python及依赖包 三、安装教程 1.Node.js安装 1.1.下载Node 1.2.安装程序 1.3.配置npm仓储和缓存 1.4. 配置环境 1.5.测试Node.j…...
用鸿蒙HarmonyOS5实现国际象棋小游戏的过程
下面是一个基于鸿蒙OS (HarmonyOS) 的国际象棋小游戏的完整实现代码,使用Java语言和鸿蒙的Ability框架。 1. 项目结构 /src/main/java/com/example/chess/├── MainAbilitySlice.java // 主界面逻辑├── ChessView.java // 游戏视图和逻辑├── …...
