初始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,饿汉模式,懒汉模式,指令重排序 的学习之旅 就到此结束啦!我们下一期再一起学习吧!

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

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

相关文章

在线预览 Word 文档

引言 随着互联网技术的发展&#xff0c;Web 应用越来越复杂&#xff0c;用户对在线办公的需求也日益增加。在许多业务场景中&#xff0c;能够直接在浏览器中预览 Word 文档是一个非常实用的功能。这不仅可以提高用户体验&#xff0c;还能减少用户操作步骤&#xff0c;提升效率…

C++ 优先算法 —— 查找总价格为目标值的两个商品(双指针)

目录 题目 &#xff1a;查找总价格为目标值的两个商品 1. 题目解析 2. 算法原理 Ⅰ 暴力枚举 Ⅱ 双指针算法 3. 代码实现 暴力枚举 双指针算法 题目 &#xff1a;查找总价格为目标值的两个商品 1. 题目解析 题目截图&#xff1a; 这道题的一个关键的地方&#xff0c;它先…

【操作系统】基于环形队列的生产消费模型

这篇博客的重点在于代码实现&#xff0c;理论部分请看CSDN 一、单生产单消费 1.环形队列的实现 单生产单消费的情况下&#xff0c;我们只需要维护生产者和消费者之间的互斥和同步关系即可 将环形队列封装成一个类&#xff1a;首先给出整体框架&#xff0c;接着会说明每一个…

【Android】Activity组件通信

文章目录 1.使用Intent传递数据2.使用Bundle传递复杂数据3.startActivityForResult 和 onActivityResult4.使用ViewModel共享数据 在Android中&#xff0c;Activity之间的通信是一个常见且重要的任务。以下是一些常用的方法来实现Activity之间的数据传递和通信&#xff1a; 1.使…

如何在Linux环境中的Qt项目中使用ActiveMQ-CPP

文章目录 代码1&#xff1a;消费者代码2&#xff1a;生成者 之前在Linux下的qt程序中使用activeMQ的时候也是用了很多时间去研究&#xff0c;本来想的是好好记录一下&#xff0c;但是当时顾着写代码。很多细节也不想再去走一遍了。大概写一下怎么使用就行了。注意&#xff1a;一…

Qt QCheckBox、QPushButton和QRadioButton详解

QCheckBox&#xff08;复选框&#xff09; 功能&#xff1a;QCheckBox用于创建一个复选框控件&#xff0c;允许用户从多个选项中选择多个。 属性&#xff1a; checkable&#xff1a;决定复选框是否可以被选中或取消选中。checked&#xff1a;表示复选框当前的选中状态&#…

6、显卡品牌分类介绍:技嘉 - 计算机硬件品牌系列文章

技嘉科技是一家以主板、‌显卡在业界缔造无以撼动的地位的科技公司&#xff0c;‌其核心理念是「‌技术创新、‌质量稳定」‌的高标准。‌技嘉专注于关键技术研发&#xff0c;‌其经营范围涵盖家用、‌商用、‌电竞等多元科技领域。‌通过应用突破性的专利技术&#xff0c;‌技…

自编以e为底的指数函数exp,性能接近标准库函数

算法描述&#xff1a; (1). 先做自变量x的范围检查&#xff0c;对于双精度浮点数&#xff0c;自变量不能超出(-1022ln2, 1024ln2)(-708.39, 709.78)&#xff0c;否则exp(x)会溢出。对于单精度浮点数&#xff0c;自变量不能超出(-126ln2, 128ln2)(-87.33, 88.72). 自己使用此函数…

es安装拼音分词后Kibana出现内存错误

出现错误 今天在安装es的拼音分词器&#xff0c;并重启es容器后&#xff0c;登录Kibana无法使用&#xff0c;查询日志发现如下报错 Waiting until all Elasticsearch nodes are compatible with Kibana before starting saved objects migrations... | typelog timestamp2024…

前端react面试基础知识(II)

这些问题涵盖了 React 的很多核心概念和实际应用场景。下面是针对每个问题的详细回答&#xff1a; 1. **React 项目中&#xff0c;如何动态改变组件的 class 来切换样式?** 可以通过条件判断或者状态&#xff08;state&#xff09;来动态改变组件的 class。例如&#xff0c;使…

Day 42 || 完全背包、518. 零钱兑换 II 、 377. 组合总和 Ⅳ、70. 爬楼梯 (进阶)

完全背包 题目链接&#xff1a;卡码网第52题 思路&#xff1a;和之前01背包一样&#xff0c;但是物品可以无限放置&#xff0c;所以之前二维数组中的背包容量是倒序遍历的&#xff0c;现在可以正序遍历即可重复放入。 import java.util.Scanner; public class Main {public …

数据结构-二叉树中的递归

目录 前言 简单手撕二叉树 二叉树节点的求解 二叉树叶子节点的求解 二叉树高度 二叉树第K层节点的个数 二叉树查找值为X的节点 结束语 前言 在这里说声抱歉&#xff0c;好久没更新数据结构了&#xff0c;二叉树的相关内容还没有更新完&#xff0c;是小编的失职&#xff…

在基于AWS EC2的云端k8s环境中 搭建开发基础设施

中间件下载使用helm,这里部署的都是单机版的 aws-ebs-storageclass.yaml apiVersion: storage.k8s.io/v1 kind: StorageClass metadata:name: aws-ebs-storageclass provisioner: kubernetes.io/aws-ebs parameters:type: gp2 # 选择合适的 EBS 类型&#xff0c;如 gp2、io1…

Apache Calcite - 查询优化之自定义优化规则

RelOptRule简介 为了自定义优化规则&#xff0c;我们需要继承RelOptRule类。org.apache.calcite.plan.RelOptRule 是 Apache Calcite 中的一个抽象类&#xff0c;用于定义优化规则。优化规则是用于匹配查询计划中的特定模式&#xff0c;并将其转换为更优化的形式的逻辑。通过继…

2024网鼎杯青龙组wp:Crypto1

题目 附件内容如下 from Crypto.Util.number import * from secret import flag from Cryptodome.PublicKey import RSAp getPrime(512) q getPrime(512) n p * q d getPrime(299) e inverse(d,(p-1)*(q-1)) m bytes_to_long(flag) c pow(m,e,n) hint1 p >> (51…

Python 单元测试中的 Mocking 与 Stubbing:提高测试效率的关键技术

在软件开发过程中&#xff0c;单元测试是确保代码质量的重要环节。为了实现高效的单元测试&#xff0c;我们常常需要隔离待测试的代码与其外部依赖。这时候&#xff0c;Mocking&#xff08;模拟&#xff09;和 Stubbing&#xff08;桩&#xff09;技术就显得尤为重要。这两种技…

Golang | Leetcode Golang题解之第528题按权重随机选择

题目&#xff1a; 题解&#xff1a; type Solution struct {pre []int }func Constructor(w []int) Solution {for i : 1; i < len(w); i {w[i] w[i-1]}return Solution{w} }func (s *Solution) PickIndex() int {x : rand.Intn(s.pre[len(s.pre)-1]) 1return sort.Searc…

3D打印机 屏幕的固定挂钩断后的一次自己修复经历

引子 3D打印机的屏幕固定挂钩断了 这次确实不知道咋断的&#xff0c;这可咋办呢&#xff0c;到网上看了一下&#xff0c;一个屏幕要2佰多&#xff0c;有些小贵&#xff0c;要不就自己修修吧&#xff0c;打个挂钩按上&#xff0c;说干就干。 正文 freecad的设计图如下【其中各…

PHP合成图片,生成海报图,poster-editor使用说明

之前写过一篇使用Grafika插件生成海报图的文章&#xff0c;但是当我再次使用时&#xff0c;却发生了错误&#xff0c;回看Grafika文档&#xff0c;发现很久没更新了&#xff0c;不兼容新版的GD&#xff0c;所以改用了intervention/image插件来生成海报图。 但是后来需要对海报…

Java基于微信小程序的美食推荐系统(附源码,文档)

博主介绍&#xff1a;✌程序猿徐师兄、8年大厂程序员经历。全网粉丝15w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;…