【多线程奇妙屋】“线程等待” 专讲,可不要只会 join 来线程等待哦, 建议收藏 ~~~

邂逅岁月 2024-10-25 08:35:02 阅读 80

本篇会加入个人的所谓鱼式疯言

❤️❤️❤️鱼式疯言:❤️❤️❤️此疯言非彼疯言

而是理解过并总结出来通俗易懂的大白话,

小编会尽可能的在每个概念后插入鱼式疯言,帮助大家理解的.

🤭🤭🤭可能说的不是那么严谨.但小编初心是能让更多人能接受我们这个概念 !!!

在这里插入图片描述

前言

线程等待机制是多线程编程中一个至关重要的概念,它允许程序在特定条件下暂停线程的执行,直到满足某些条件。这种机制不仅提高了资源的利用率,还使得程序的执行更加高效和有序

目录

线程等待

join() 等待

wait()等待

线程状态

一. 线程等待

1. 线程等待的初识

我们知道, 并发编程的是: 随机调度,抢占式执行。

虽然 无法决定线程的执行顺序 ,但是我们可以让 后执行的线程等待先执行的线程 , 在 <code>先执行线程 执行过程中, 后执行线程一直处于阻塞等待 。 直到 先执行的线程 执行完毕了 , 后执行的线程才 开始执行

而在Java的标准库中就提供了 一系列的 API 来执行线程等待: join() , wait(),以及 sleep() 等…

下面让小编好好介绍一下吧 💕 💕 💕 💕

二. join() 等待

join() 方法是 Thread中的成员方法, 主要用于等待调用 join() 那个线程的结束

1. 代码演示

public class JoinDemo1 {

/**

* 主线程等待

*

* 1. 用到 对象1.join

* 2. 在 main 线程的作用域 调用 join 方法时,就需要等待 对象1 先执行

* 3. 自身处于 阻塞状态,等待 调用 join方法的对象先执行完

* @param args

* @throws InterruptedException 等待方法需要抛出的异常

*

*

*/

public static void main(String[] args) throws InterruptedException {

Thread t1 = new Thread(()-> {

for (int i = 0; i < 4; i++) {

System.out.println("t1正在执行...");

}

});

t1.start();

System.out.println("main 线程正在等待...");

// 等待 t1 执行完再执行 main 线程

t1.join();

// 一般情况创建 进程的同时

// 主线程先获取资源 抢占的更快

for (int i = 0; i < 4; i++) {

System.out.println("main 线程正在执行...");

}

System.out.println("main 线程执行完毕!");

}

}

在这里插入图片描述

如上图:

创建线程t1 , 并调用 <code>join () , 这时从打印的结果就可以看出, 当调用 join() 后, 主线程就会进入 阻塞等待 的状态, 直到线程t1 执行完毕才执行 主线程的业务逻辑

这时我们就要理解为啥是主线程 等待 t1 线程, 明明是 t1 线程 调用了join 方法 , 为啥是主线程等待 t1 线程 呢?

2. 原理分析

其实是这样子的, 等待者 是在那个线程下调用的那个线程为 等待者

比如上述过程中 , 在 主线程 t1 调用了 join 方法 , 这时就划分出了,在 哪个线程环境中调用 join, 那个线程就是等待者 , 而那个线程对象调用 join方法 , 那么这个 线程对象就是被等待者 , 例如上面的 t1

可能小伙伴们还没有理解吧 🤔 🤔 🤔 🤔

下面小编举个栗子来理解吧

有一天小编下午没课, 而女神下午有课, 我和女神约好下午放学去吃麻辣烫

女神是五点零五放学的

如果我四点五十到达她教室门口就需要等~ 也就是女生调用了 join(), 对我来说, 我时间没有把握好那么就 需要等待的

如果我四点十分到达她教室门口, 就不需要等待 , 女神一出来就可以看到我, 然后一起去吃麻辣烫, 对于我而言我时间 把握的刚刚好 , 这时即使女神调用 join() 方法, 我也就 不需要等待

对于两个线程好理解, 如果是对于 三个以及三个以上的多个线程 该怎么去理解呢 ? ? ?

3. 多个线程join等待

public class JoinDemo2 {

/**

* 多个线程之间的线程等待

* 在哪个线程的 作用域中调用哪个,那个线程对等待 调用 join 的那个对象

*

* 在哪个作用域中执行, 哪个处于阻塞状态 ,

* 哪个对象调用, 就优先执行哪个对象。

*

*

* @param args

* @throws InterruptedException

*

*

*/

public static void main(String[] args) throws InterruptedException {

Thread t1 = new Thread(()-> {

for (int i = 0; i < 3; i++) {

System.out.println("t1正在执行...");

}

});

Thread t2 = new Thread(()-> {

// 如果在 t2 的线程中进行t1 线程等待

for (int i = 0; i < 3; i++) {

System.out.println("t2正在执行...");

}

});

System.out.println("main 线程正在等待 t1 和 t2 的线程的执行... ");

t1.start();

t2.start();

// main线程 会等待 t1 和 t2 先执行

// 但是 t1 和 t2 之间是不存在 线程等待的

t1.join();

t2.join();

System.out.println("main 线程 等待 t1 和 t2 的线程的执行结束!!!");

// 表明 main 在前期会抢占更快

for (int i = 0; i < 3; i++) {

System.out.println("main 正在执行...");

}

}

}

在这里插入图片描述

如上图:

当出现三个线程的情况, 这时还是需要抓着本质, 在main 线程中调用创建 线程 t1 和 t2 , 并让 t1 和 t2 分别调用 <code>join() 方法, 按照上面的理解, 就是 线程先等待 t1 和 t2 线程先结束 , 然后再执行 主线程的逻辑代码 是完全正确的。

这时我们需要考虑一点: t1t2 是有 等待 关系吗? 答案是否定的, 对于 t1 和 t2 而言是没有等待关系的, 还是并发执行的: 随机调度, 抢占式执行 的过程 。

鱼式疯言

补充细节

以上代码并不是说, 只能让主线程等待其他线程的执行。 也可以在其他线程中让需要等待的线程去调用 join 方法

在这里插入图片描述

如上图:

其实这里要达到让 t1 执行完然后再执行 t2 , 最后在执行 main 线程的这样的串行执行 , 不仅要在 <code>main 线程 中调用各自 join() 方法 ,先让 main 等待 t1 和 t2 线程的结束从而保证自己是最后执行的一个 , 也要 在 t2 线程中让 t1调用 join() , 让 t2 也等待 t1 , 这样才能保证 t1 是第一个被执行结束 的。


上述使用都是没有参数的join() 方法, 其实还有有参数版本的 join(), 下面让我们来看看吧~ 💥 💥 💥 💥

4. 有参数的join() 方法

对于 无参数的join() , 就需要 无限的等待需要等待的线程结束

如果该线程一直不结束或者出现了 BUG , 异常退出了, 就会让后面的代码就一直执行不到,一直处于 阻塞的状态

但是如果让设置一个 指定的时间 就能让线程在指定时间内等待, 这样就不会因为一直等待而出现执行不到的问题

class JoinDemo {

public static void main(String[] args) throws InterruptedException {

Thread t1 = new Thread(()-> {

for (int i = 0; i < 4; i++) {

System.out.println("t1正在执行...");

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

throw new RuntimeException(e);

}

}

});

t1.start();

System.out.println("main 线程正在等待...");

// 等待 t1 执行完再执行 main 线程

t1.join(1000);

// 一般情况创建 进程的同时

// 主线程先获取资源 抢占的更快

for (int i = 0; i < 2; i++) {

System.out.println("main 线程正在执行...");

}

System.out.println("main 线程执行完毕!");

}

}

在这里插入图片描述

如上图: join 在 <code>1000 ms 也就是 1秒内, 先执行一秒都 t1 线程中的任务 , 然后 main 线程和 t1 线程 并发执行

鱼式疯言

补充说明

在这里插入图片描述

还有一个 纳秒级别的, 小伙伴们了解即可, 不需要重点掌握哦~

三. wait()等待

1. 线程饿死

在使用wait() 之前, 我们先得熟悉一个概念问题——线程饿死

什么是线程饿死呢?

在这里插入图片描述

如上图, 一群滑稽老铁在排队使用ATM 机, 这时有一个滑稽老铁进去取钱了, 进去的时候发现ATM机里面没钱了(ATM机的钱毕竟还是有限的), 那么这时这位滑稽老铁就要等待银行工作人员在后台去取钱加入到ATM机中, 但是这位滑稽老铁刚出ATM机时, 又想是不是ATM机中的钱加入好了, 于是又进去, 发现里面还是没钱, 于是出来了 , 然后又想ATM机中的钱是不是有了, 于是又进去。

当这位滑稽老铁进进出出, 这时就会产生一个问题,其他滑稽老铁怎么办? 对应的其他线程该怎么执行呢?

如果一个线程一会 又竞争锁一会又释放锁, 又竞争又释放, 这时其他的线程就会一直处于 阻塞等待的状态 , 其他线程就 <code>无法执行到自己的业务逻辑 , 就产生了BUG , 而我们把这种情况称之为 线程饿死”

2. wait 方法解决线程饿死

对于上述线程饿死的问题, 我们就可以使用 wait 方法来使用,我们先来看看演示效果吧~

<1>. 代码演示

class DemoWait {

public static Object locker = new Object();

public static void main(String[] args) throws InterruptedException {

Thread t = new Thread(()->{

// 进行加锁并等待

synchronized(locker) {

try {

locker.wait();

} catch (InterruptedException e) {

throw new RuntimeException(e);

}

}

System.out.println("hello t");

});

// 创建线程

t.start();

// 先让 t 上锁等待

Thread.sleep(100);

System.out.println("开始打印t");

synchronized(locker) {

// 唤醒t

locker.notify();

}

// 等待t 结束

t.join();

// 打印日志

System.out.println("t 打印完毕!");

}

}

在这里插入图片描述

如上图, 这里的具体流程:

首先对线程 t 里的业务进行 wait 进行加锁,让 <code>线程 t 一直处于 阻塞等待的状态

然后在主线程中创建线程 , 并且在 同一对象加锁下 使用 notify() 对线程t 进行 唤醒

线程t 继续执行, 也就 重新竞争锁对象

关于还不了解锁以及加锁操作的小伙伴可以回顾下面这篇文章哦

在这里插入图片描述

加锁操作文章详解

鱼式疯言

<code>wait 在 等待一个有缘人唤醒 , 那个有缘人就是 notify

就好像 睡美人 在等待 她的王子的唤醒

<2>. 原理分析

对于上述线程饿死问题, 我们不能让一个线程一边释放锁又一般拿着锁, 使用wait() 的原理就是:

拿着锁对象的那个线程先释放锁

然后一直处于 阻塞等待的状态

直到其他线程使用 notify() 方法 对该 锁对象 来唤醒 wait() 才执行下面的 代码逻辑,最终重新 竞争锁对象

使用过程需要注意的问题

对于wait 的原理而言,是先要释放锁, 释放锁的前提是 先得进行加锁 , 不加锁就无法谈及释放锁, 有加锁才能释放锁否则就会出现如下情况:

加粗样式

是的, 只有当 加锁之后才能释放了锁 , 当释放锁之后, 锁就可以由其他线程来竞争 , 此时当前线程一直处于 <code>阻塞等待的状态就无法竞争锁对象 , 就不会出现 线程饿死 的现象。

notify() 方法也要加锁, 在多线程中, 一个线程加锁,另一个线程不加锁, 是无法发生阻塞的, 如果 wait 没有释放锁, 即使执行到 notify()没有什么意义

加锁的时间的问题, 对于这个点是要把握好的, 就是说如果先执行到 notify() , 然后再加锁, 这时就早错过了 notify() 的唤醒时机, 这时wait线程就会一直处于 阻塞等待 的时候。

如图会出现 这种情况 :

在这里插入图片描述

如何解决这个问题, 我们只需要先确保 wait 先执行, 然后再执行到 notify 即可, 只需要让 notify() 慢点执行, 再前面加个 <code>sleep 即可(如下行代码)。

Thread.sleep(100);

System.out.println("开始打印t");

synchronized(locker) {

// 唤醒t

locker.notify();

}

举个栗子说明吧 ~

在这里插入图片描述

当上面的滑稽老铁需要等待ATM中加钱的突然临时走开,

但是在他离开的那段时间,银行的工作人员以及 <code>把钱加好到 ATM机中 , 这时他在回来的时候, 并 没有收到这样的通知 , 就会一直等待ATM 中有钱

这样就相当于 先通知后等待, 就会发生这样的完美的错过,就会一直 等待下去

那么如果是发生这样的问题, 我们Java程序员该怎么解决呢?

下面我们来看看吧~

鱼式疯言

补充细节

对于 wait () 和notify 的使用 不仅要加锁 , 并且 锁对象必须是相同 , 在多线程中, 不仅是要有 锁对象 ,而且锁对象必须相同才能发生 阻塞等待 的情况。

这个的 wait 只能用一次,否则就会有 多个等待 , 一个 notify 是无法唤醒多个 wait的 。

在这里插入图片描述

但是 <code>notify 可以使用多次, 唤醒可以多次, 即使没有多个锁也无妨。

在这里插入图片描述

3. 带参数的 wait 方法

竟然有可能会发生错过, 一旦错过唤醒的时机, 就会一直阻塞等待…

那么我们不妨设定一个 阻塞等待的时间 , 在这个时间内如果有 <code>notify() 唤醒 , 就 继续执行 ; 如果超过了这个时间还 没有 notify 来唤醒 , 就 自动解除阻塞等待状态 , 继续 执行后面的代码

在这里插入图片描述

上图这是错过了 notify通知的情况

那么我们加个参数试试…

在这里插入图片描述

这时我们把参数设定了 <code>500 ms(毫秒) 也就是 0.5 妙(s) , 即使我们 错过了notify的唤醒时机 , 超过了这段时间,我们也会 自动唤醒 的。

鱼式疯言

对于 有参数和无参数的wait 而言, 都是 根据具体情况具体使用的 , 不能说 哪个更好久用哪个 的情况。

4. wait() 与 sleep() 的区别

虽然 waitsleep 都能让 看起来都能让线程等待, 但其实有本质的区别~

wait 需要 synchronized 先加锁然后解锁, sleep 不需要

wait 的让线程进入阻塞等待, 等待着 notify 的唤醒 , 而 sleep 是让 线程进入休眠 , 通过 isintterrupted 线程终止的方式 停止休眠

waitObject对象 下的方法, 而sleep 是Thread的 静态方法(类方法)

5. wait 与 notify 的实际运用

如果我们要使用多线程俺顺序打印 A , B , C 该怎么操作呢?

class Demo11 {

private static Object locker1 = new Object();

private static Object locker2 = new Object();

private static Object locker3 = new Object();

public static void main(String[] args) throws InterruptedException {

// 在各自线程中进行等待

Thread t1 =new Thread(()->{

synchronized(locker1) {

try {

locker1.wait();

} catch (InterruptedException e) {

throw new RuntimeException(e);

}

}

System.out.println("A");

});

Thread t2 =new Thread(()->{

synchronized(locker2) {

try {

locker2.wait();

} catch (InterruptedException e) {

throw new RuntimeException(e);

}

}

System.out.println("B");

});

Thread t3 =new Thread(()->{

synchronized(locker3) {

try {

locker3.wait();

} catch (InterruptedException e) {

throw new RuntimeException(e);

}

}

System.out.println("C");

});

t1.start();

t2.start();

t3.start();

// 在主线程中分别唤醒

Thread.sleep(100);

synchronized (locker1) {

locker1.notify();

}

Thread.sleep(100);

synchronized (locker2) {

locker2.notify();

}

Thread.sleep(100);

synchronized (locker3) {

locker3.notify();

}

}

}

在这里插入图片描述

上面的流程其实很简单, 就是把每个线程都加上 不同对象的wait , 然后notify 按照不同的对象延时的按顺序 的使用notify 唤醒即可。

这里小编只是写出我个人 的方案, 小伙伴们如果有更好的方案来按 顺序输出 A, B , C 的话, 欢迎评论区留言哦 ~

四. 线程状态

1. 线程状态的初识

对于线程状态,我们一般只大体上分为两种:

<code>就绪: 正在CPU上调度执行或准备在CPU上调度执行

阻塞阻塞等待 状态

鱼式疯言

可以理解为一个是执行的状态, 一个是休息状态


但是在Java中, 我们又把线程状态分的更细, 下面让我们

2. 五种线程状态

NEW 状态: 是创建好 Thread 对象, 但还没有在 系统内核中创建线程 (也就是 没有调用start 方法)。

TERMINATED 状态: 就绪状态也就是 正在CPU上调度执行和准备在CPU上调度执行

BLOCKED 状态: 也就是阻塞等待状态,(由于锁竞争引起的阻塞等待)

TIME-WAITING 状态: 带有超时间, 由于调用了 sleep()带参数版本wait() 或 join() 引起的阻塞等待。

WAITING 状态: 由于调用 wait()join() 方法需要 notify() 唤醒的阻塞等待状态。

这五种状态小伙伴了解即可, 混个眼熟急救可以哦 ~ ~ ~

总结

. 线程等待: 熟悉线程等待是: 只能控制哪个线程先结束 , 而 不能控制线程的执行顺序 的概念。

. join() 等待: 掌握 join()方法的本质:谁调用 join 谁就是被等待的那个线程, 而在哪个线程的作用域中去调用, 哪个线程就进行等待。 并含有带参数版本的join的方法。

. wait()等待: 对于wait的本质理解: 先进行加锁才能释放锁 , 然后一直<code>阻塞等待, 直到 notify 唤醒阻塞, 使用 waitnotify 这两个线程都需要进行 加上同一把锁对象 的锁。

. 线程状态: Java 细分出五个线程状态: 只实例化对象并未创建线程 的状态的 NEW 状态, 处于就绪状态的 TERMINATED 状态 , 以及 发生锁竞争BLOCKED 状态 ,以及有 <code>超时阻塞 的 TIME-WAITING 状态需要被唤醒WAITING 状态

如果觉得小编写的还不错的咱可支持 三连 下 (定有回访哦) , 不妥当的咱请评论区 指正

希望我的文章能给各位宝子们带来哪怕一点点的收获就是 小编创作 的最大 动力 💖 💖 💖

在这里插入图片描述



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。