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

ops/2024/10/29 23:26:37/

找往期文章包括但不限于本期文章中不懂的知识点:

个人主页:我要学编程(ಥ_ಥ)-CSDN博客

所属专栏:JavaEE

目录

wait、notify 方法

多线程练习

单例模式

饿汉模式

懒汉模式

指令重排序 


wait、notify 方法

wait 和 我们前面学习的sleep、join方法一样,也是让线程阻塞,但是其可以被notify方法唤醒,但是sleep是被Interrupt给提前唤醒或者指定时间过了之后自动被唤醒,并且会抛出异常。且 join 是一个线程等待另一个线程,并且要 被等待的线程彻底执行完成之后,等待的线程才会从阻塞的中被唤醒重新执行。

wait方法在使用时,要和synchronized一起搭配使用,因为其是先对调用它的对象进行解锁,阻塞,在被唤醒之后,在进行加锁操作。

java">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 方法阻塞的线程将不会在处于阻塞状态。

java">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:

java">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版本:

java">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即可,这样在外部就不能创建实例了。 

单例模式中,最常见的就是饿汉模式与懒汉模式。 

饿汉模式

饿汉模式,主要体现在"饿"字上,因为其是迫不及待的去创建类的实例。

代码演示:

java">// 饿汉模式
class SingleTon {// 迫不及待的创建实例private static SingleTon singleTon = new SingleTon();public static SingleTon getInstance() {return singleTon;}// 单例模式的构造方法一定是private修饰的private SingleTon() {}
}

这里创建类的实例是通过创建一个静态的成员变量来实现的,而静态的成员变量是类在加载时,就会被创建,即JVM中有这个类存在的痕迹的话,那么这个实例就会存在。 因此,以"饿"得名。

我们也可以去检查这个饿汉模式是否创建成功,主要检查是否是单例模式。

java">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}
}

从上面的程序运行的结果,可以得知:一个进程中不能实例化多个对象,符合单例模式的特征。

懒汉模式

懒汉模式,主要体现在"懒"字上,只有当迫不得已时,才去创建实例。

代码演示:

java">// 懒汉模式
class SingleTonLazy {// 迫不得已才创建实例private static SingleTonLazy singleTonLazy = null;public static SingleTonLazy getInstance() {if (singleTonLazy == null) {singleTonLazy = new SingleTonLazy(); // 一定要把对象保留下来}return singleTonLazy;}// 单例模式的构造方法一定是私有的private SingleTonLazy() {}
}

懒汉模式只有当外部调用getInstance方法时,才会去创建实例,否则就不会创建实例。 

同样也可以去测试这个懒汉模式是否创建成功。

java">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 语句之中,其认为还没有创建对象。

因此,我们得对上述代码进行加锁操作。

java">// 懒汉模式
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,而不会再去进行加锁操作了,这样性能就提升了不少。

java">    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 关键字了。这个关键字既可以避免 内存可见性的问题,也可以避免指令重排序的问题。

java">private static volatile SingleTonLazy singleTonLazy = null;

好啦!本期 初始JavaEE篇——多线程(4):wait、notify,饿汉模式,懒汉模式,指令重排序 的学习之旅 就到此结束啦!我们下一期再一起学习吧!


http://www.ppmy.cn/ops/129439.html

相关文章

数据采集与数据分析:数据时代的双轮驱动

“在当今这个数据驱动的时代&#xff0c;信息已成为企业决策、市场洞察、科学研究等领域不可或缺的核心资源。而爬虫数据采集与数据分析&#xff0c;作为数据处理链条上的两大关键环节&#xff0c;它们之间相辅相成&#xff0c;共同构成了数据价值挖掘的强大引擎。” 爬虫数据采…

MySQL Workbench工作台汉化

一、下载汉化包 通过百度网盘分享的文件&#xff1a;MySQL汉化包.rar 链接&#xff1a;https://pan.baidu.com/s/1PaJSU9dvVnQQWEESHSue5Q 二、汉化过程 注意&#xff1a;替换之前一定要记得把两个文件复制出来存着&#xff0c;防止替换失败修改了文件 找到MySQL的工作台da…

武器检测与分割系统:全程教学跟进

武器检测与分割系统源码&#xff06;数据集分享 [yolov8-seg-SPPF-LSKA&#xff06;yolov8-seg-FocalModulation等50全套改进创新点发刊_一键训练教程_Web前端展示] 1.研究背景与意义 项目参考ILSVRC ImageNet Large Scale Visual Recognition Challenge 项目来源AAAI Glob…

《Python游戏编程入门》注-第4章1

《Python游戏编程入门》的第4章是“用户输入&#xff1a;Bomb Cathcer游戏”&#xff0c;通过轮询键盘和鼠标设备状态实现Bomb Cathcer游戏。 1 Bomb Cathcer游戏介绍 “4.1 认识Bomb Cathcer游戏”内容介绍了Bomb Cathcer游戏的玩法&#xff0c;即通过鼠标来控制红色“挡板”…

Linux 重启命令全解析:深入理解与应用指南

Linux 重启命令全解析&#xff1a;深入理解与应用指南 在 Linux 系统中&#xff0c;掌握正确的重启命令是确保系统稳定运行和进行必要维护的关键技能。本文将深入解析 Linux 中常见的重启命令&#xff0c;包括功能、用法、适用场景及注意事项。 一、reboot 命令 功能简介 re…

leetcode 763.划分字母区间

思路&#xff1a;贪心 其实这个题目并不难&#xff0c;只需要分析出来每一个字母最后出现的坐标就行。 我们根据字母最后出现的坐标数来判断最后划分的字符串。 比如说&#xff0c;字符串前面有abc&#xff0c;这三个字母最后出现的地方就是这个位置&#xff0c;那么我们直接…

初识Vue

一、Vue介绍 Vue(读音/vjuː/&#xff0c;类似于view) 是一套用于构建前后端分离的框架。刚开始是由国内优秀选手尤雨溪开发出来的&#xff0c;目前是全球“最”流行的前端框架。使用vue开发网页很简单&#xff0c;并且技术生态环境完善&#xff0c;社区活跃&#xff0c;是前后…

1.机器人抓取与操作介绍-深蓝学院

介绍 操作任务 操作 • Insertion • Pushing and sliding • 其它操作任务 抓取 • 两指&#xff08;平行夹爪&#xff09;抓取 • 灵巧手抓取 7轴 Franka 对应人的手臂 6轴 UR构型去掉一个自由度 课程大纲 Robotic Manipulation 操作 • Robotic manipulation refers…