【多线程奇妙屋】“线程等待” 专讲,可不要只会 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 线程先结束 , 然后再执行
主线程的逻辑代码
是完全正确的。
这时我们需要考虑一点:
t1
和t2
是有等待
关系吗? 答案是否定的, 对于 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() 的区别
虽然 wait
和 sleep
都能让 看起来都能让线程等待, 但其实有本质的区别~
wait
需要synchronized
先加锁然后解锁,sleep
不需要
wait
的让线程进入阻塞等待, 等待着 notify 的唤醒 , 而sleep
是让 线程进入休眠 , 通过isintterrupted
线程终止的方式 停止休眠 。
wait
是 Object对象 下的方法, 而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
唤醒阻塞, 使用 wait
和 notify
这两个线程都需要进行 加上同一把锁对象 的锁。
. 线程状态: Java 细分出五个线程状态: 只实例化对象并未创建线程 的状态的 NEW 状态, 处于就绪状态的 TERMINATED 状态 , 以及 发生锁竞争 的 BLOCKED 状态 ,以及有 <code>超时阻塞 的 TIME-WAITING 状态,需要被唤醒
的 WAITING 状态。
如果觉得小编写的还不错的咱可支持 三连 下 (定有回访哦) , 不妥当的咱请评论区 指正
希望我的文章能给各位宝子们带来哪怕一点点的收获就是 小编创作 的最大 动力 💖 💖 💖
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。