本篇会加入个人的所谓鱼式疯言
❤️❤️❤️鱼式疯言:❤️❤️❤️此疯言非彼疯言
而是理解过并总结出来通俗易懂的大白话,
小编会尽可能的在每个概念后插入鱼式疯言,帮助大家理解的.
🤭🤭🤭可能说的不是那么严谨.但小编初心是能让更多人能接受我们这个概念 !!!
前言
线程等待机制是多线程编程中一个至关重要的概念,它允许程序在特定条件下暂停线程的执行,直到满足某些条件。这种机制不仅提高了资源的利用率,还使得程序的执行更加高效和有序
目录
-
线程等待
-
join() 等待
-
wait()等待
-
线程状态
一. 线程等待
1. 线程等待的初识
我们知道, 并发编程的是: 随机调度,抢占式执行。
虽然 无法决定线程的执行顺序 ,但是我们可以让 后执行的线程等待先执行的线程 , 在
先执行线程
执行过程中, 后执行线程一直处于阻塞等待 。 直到 先执行的线程 执行完毕了 , 后执行的线程才 开始执行 。
而在Java的标准库中就提供了 一系列的 API
来执行线程等待: join()
, wait()
,以及 sleep()
等...
下面让小编好好介绍一下吧 💕 💕 💕 💕
二. join() 等待
join()
方法是 Thread中的成员方法 , 主要用于等待调用 join()
那个线程的结束
1. 代码演示
java
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 , 并调用
join ()
, 这时从打印的结果就可以看出, 当调用join()
后, 主线程就会进入阻塞等待
的状态, 直到线程t1 执行完毕才执行 主线程的业务逻辑 。
这时我们就要理解为啥是主线程 等待 t1 线程
, 明明是 t1 线程
调用了join 方法 , 为啥是主线程等待 t1 线程 呢?
2. 原理分析
其实是这样子的,
等待者
是在那个线程下调用的那个线程为 等待者比如上述过程中 , 在 主线程 t1 调用了 join 方法 , 这时就划分出了,在 哪个线程环境中调用 join, 那个线程就是等待者 , 而那个线程对象调用 join方法 , 那么这个 线程对象就是被等待者 , 例如上面的
t1
。
可能小伙伴们还没有理解吧 🤔 🤔 🤔 🤔
下面小编举个栗子来理解吧
有一天小编下午没课, 而女神下午有课, 我和女神约好下午放学去吃麻辣烫
女神是五点零五放学的
如果我四点五十到达她教室门口就需要等~ 也就是女生调用了 join()
, 对我来说, 我时间没有把握好那么就 需要等待的 。
如果我四点十分到达她教室门口, 就不需要等待 , 女神一出来就可以看到我, 然后一起去吃麻辣烫, 对于我而言我时间 把握的刚刚好 , 这时即使女神调用 join()
方法, 我也就 不需要等待 。
对于两个线程好理解, 如果是对于 三个以及三个以上的多个线程 该怎么去理解呢 ? ? ?
3. 多个线程join等待
java
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 分别调用
join()
方法, 按照上面的理解, 就是 主线程先等待 t1 和 t2 线程先结束 , 然后再执行主线程的逻辑代码
是完全正确的。
这时我们需要考虑一点:t1
和t2
是有等待
关系吗? 答案是否定的, 对于 t1 和 t2 而言是没有等待关系的 , 还是并发执行的: 随机调度, 抢占式执行 的过程 。
鱼式疯言
补充细节:
以上代码并不是说, 只能让主线程等待其他线程的执行。 也可以在其他线程中让需要等待的线程去调用 join
方法
如上图:
其实这里要达到让 t1 执行完然后再执行 t2 , 最后在执行 main 线程的这样的串行执行 , 不仅要在
main 线程
中调用各自join() 方法
,先让 main 等待 t1 和 t2 线程的结束 , 从而保证自己是最后执行的一个 , 也要 在 t2 线程中让 t1调用 join() , 让t2
也等待t1
, 这样才能保证t1 是第一个被执行结束
的。
上述使用都是没有参数的join() 方法, 其实还有有参数版本的 join(), 下面让我们来看看吧~ 💥 💥 💥 💥
4. 有参数的join() 方法
对于 无参数的join() , 就需要 无限的等待需要等待的线程结束 ,
如果该线程一直不结束或者出现了BUG
, 异常退出了, 就会让后面的代码就一直执行不到 ,一直处于阻塞的状态
。
但是如果让设置一个 指定的时间 就能让线程在指定时间内等待, 这样就不会因为一直等待而出现执行不到的问题。
java
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 在 1000 ms
也就是 1秒内, 先执行一秒都 t1 线程中的任务 , 然后 main 线程和 t1 线程
并发执行 。
鱼式疯言
补充说明 :
还有一个 纳秒级别的, 小伙伴们了解即可, 不需要重点掌握哦~
三. wait()等待
1. 线程饿死
在使用wait() 之前, 我们先得熟悉一个概念问题------线程饿死
什么是线程饿死呢?
如上图, 一群滑稽老铁在排队使用ATM 机, 这时有一个滑稽老铁进去取钱了, 进去的时候发现ATM机里面没钱了(ATM机的钱毕竟还是有限的), 那么这时这位滑稽老铁就要等待银行工作人员在后台去取钱加入到ATM机中, 但是这位滑稽老铁刚出ATM机时, 又想是不是ATM机中的钱加入好了, 于是又进去, 发现里面还是没钱, 于是出来了 , 然后又想ATM机中的钱是不是有了, 于是又进去。
当这位滑稽老铁进进出出, 这时就会产生一个问题,其他滑稽老铁怎么办? 对应的其他线程该怎么执行呢?
如果一个线程一会 又竞争锁一会又释放锁, 又竞争又释放, 这时其他的线程就会一直处于 阻塞等待的状态 , 其他线程就
无法执行到自己的业务逻辑
, 就产生了BUG , 而我们把这种情况称之为"线程饿死"
2. wait 方法解决线程饿死
对于上述线程饿死的问题, 我们就可以使用 wait 方法来使用,我们先来看看演示效果吧~
<1>. 代码演示
java
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 进行加锁 ,让
线程 t
一直处于 阻塞等待的状态 -
然后在主线程中创建线程 , 并且在 同一对象加锁下 使用
notify()
对线程t 进行唤醒
。 -
线程t 继续执行, 也就 重新竞争锁对象 。
关于还不了解锁以及加锁操作的小伙伴可以回顾下面这篇文章哦
鱼式疯言
wait
在 等待一个有缘人唤醒 , 那个有缘人就是notify
。
就好像睡美人
在等待 她的王子的唤醒。
<2>. 原理分析
- 对于上述线程饿死问题, 我们不能让一个线程一边释放锁又一般拿着锁, 使用wait() 的原理就是:
- 让 拿着锁对象的那个线程先释放锁
- 然后一直处于 阻塞等待的状态
- 直到其他线程使用
notify() 方法
对该锁对象
来唤醒wait()
才执行下面的 代码逻辑,最终重新 竞争锁对象 。
- 使用过程需要注意的问题:
- 对于wait 的原理而言,是先要释放锁, 释放锁的前提是 先得进行加锁 , 不加锁就无法谈及释放锁, 有加锁才能释放锁否则就会出现如下情况:
是的, 只有当 加锁之后才能释放了锁 , 当释放锁之后, 锁就可以由其他线程来竞争 , 此时当前线程一直处于
阻塞等待的状态就无法竞争锁对象
, 就不会出现 线程饿死 的现象。
-
notify() 方法也要加锁, 在多线程中, 一个线程加锁,另一个线程不加锁, 是无法发生阻塞的, 如果
wait 没有释放锁
, 即使执行到notify()
也 没有什么意义 。 -
加锁的时间的问题, 对于这个点是要把握好的, 就是说如果先执行到 notify() , 然后再加锁, 这时就早错过了
notify()
的唤醒时机, 这时wait线程就会一直处于阻塞等待
的时候。
如图会出现 这种情况 :
如何解决这个问题, 我们只需要先确保 wait 先执行, 然后再执行到 notify 即可 , 只需要让 notify() 慢点执行 , 再前面加个 sleep
即可(如下行代码)。
java
Thread.sleep(100);
System.out.println("开始打印t");
synchronized(locker) {
// 唤醒t
locker.notify();
}
举个栗子说明吧 ~
当上面的滑稽老铁需要等待ATM中加钱的突然临时走开,
但是在他离开的那段时间,银行的工作人员以及 把钱加好到 ATM机中
, 这时他在回来的时候, 并 没有收到这样的通知 , 就会一直等待ATM 中有钱...
这样就相当于 先通知后等待 , 就会发生这样的完美的错过,就会一直 等待下去 。
那么如果是发生这样的问题, 我们Java程序员该怎么解决呢?
下面我们来看看吧~
鱼式疯言
补充细节 :
- 对于
wait () 和notify
的使用 不仅要加锁 , 并且 锁对象必须是相同 , 在多线程中, 不仅是要有锁对象
,而且锁对象必须相同才能发生阻塞等待
的情况。
这个的 wait 只能用一次,否则就会有 多个等待
, 一个 notify 是无法唤醒多个 wait的 。
但是 notify
可以使用多次, 唤醒可以多次, 即使没有多个锁也无妨。
3. 带参数的 wait 方法
竟然有可能会发生错过, 一旦错过唤醒的时机, 就会一直阻塞等待...
那么我们不妨设定一个 阻塞等待的时间 , 在这个时间内如果有
notify() 唤醒
, 就 继续执行 ; 如果超过了这个时间还 没有 notify 来唤醒 , 就 自动解除阻塞等待状态 , 继续执行后面的代码
。
上图这是错过了 notify通知的情况
那么我们加个参数试试...
这时我们把参数设定了
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 该怎么操作呢?
java
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. 线程状态的初识
对于线程状态,我们一般只大体上分为两种:
-
就绪
: 正在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的本质理解: 先进行加锁才能释放锁 , 然后一直
阻塞等待
, 直到notify
唤醒阻塞, 使用wait
和notify
这两个线程都需要进行 加上同一把锁对象 的锁。 -
. 线程状态 : Java 细分出五个线程状态: 只实例化对象并未创建线程 的状态的 NEW 状态 , 处于就绪状态的 TERMINATED 状态 , 以及 发生锁竞争 的 BLOCKED 状态 ,以及有
超时阻塞
的 TIME-WAITING 状态 ,需要被唤醒
的 WAITING 状态。
如果觉得小编写的还不错的咱可支持 三连 下 (定有回访哦) , 不妥当的咱请评论区 指正
希望我的文章能给各位宝子们带来哪怕一点点的收获就是 小编创作 的最大 动力 💖 💖 💖