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

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

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

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

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

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

前言

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

目录

  1. 线程等待

  2. join() 等待

  3. wait()等待

  4. 线程状态

一. 线程等待

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 线程先结束 , 然后再执行 主线程的逻辑代码 是完全正确的。
这时我们需要考虑一点: t1t2 是有 等待 关系吗? 答案是否定的, 对于 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 打印完毕!");
    }
}

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

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

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

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

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

加锁操作文章详解

鱼式疯言

wait等待一个有缘人唤醒 , 那个有缘人就是 notify
就好像 睡美人 在等待 她的王子的唤醒。

<2>. 原理分析

  • 对于上述线程饿死问题, 我们不能让一个线程一边释放锁又一般拿着锁, 使用wait() 的原理就是:
  1. 让 拿着锁对象的那个线程先释放锁
  2. 然后一直处于 阻塞等待的状态
  3. 直到其他线程使用 notify() 方法 对该 锁对象 来唤醒 wait() 才执行下面的 代码逻辑,最终重新 竞争锁对象 。
  • 使用过程需要注意的问题
  1. 对于wait 的原理而言,是先要释放锁, 释放锁的前提是 先得进行加锁 , 不加锁就无法谈及释放锁, 有加锁才能释放锁否则就会出现如下情况:

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

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

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

如图会出现 这种情况 :

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

java 复制代码
        Thread.sleep(100);

        System.out.println("开始打印t");
        synchronized(locker) {
//            唤醒t
            locker.notify();
        }

举个栗子说明吧 ~

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

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

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

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

下面我们来看看吧~

鱼式疯言

补充细节

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

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

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

3. 带参数的 wait 方法

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

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

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

那么我们加个参数试试...

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

鱼式疯言

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

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

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

  1. wait 需要 synchronized 先加锁然后解锁, sleep 不需要
  2. wait 的让线程进入阻塞等待, 等待着 notify 的唤醒 , 而 sleep 是让 线程进入休眠 , 通过 isintterrupted 线程终止的方式 停止休眠 。
  3. 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. 线程状态的初识

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

  1. 就绪: 正在CPU上调度执行或准备在CPU上调度执行

  2. 阻塞阻塞等待 状态

鱼式疯言

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


但是在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 唤醒阻塞, 使用 waitnotify 这两个线程都需要进行 加上同一把锁对象 的锁。

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

如果觉得小编写的还不错的咱可支持 三连 下 (定有回访哦) , 不妥当的咱请评论区 指正
希望我的文章能给各位宝子们带来哪怕一点点的收获就是 小编创作 的最大 动力 💖 💖 💖

相关推荐
Oneforlove_twoforjob4 分钟前
【Java基础面试题033】Java泛型的作用是什么?
java·开发语言
向宇it21 分钟前
【从零开始入门unity游戏开发之——C#篇24】C#面向对象继承——万物之父(object)、装箱和拆箱、sealed 密封类
java·开发语言·unity·c#·游戏引擎
小蜗牛慢慢爬行23 分钟前
Hibernate、JPA、Spring DATA JPA、Hibernate 代理和架构
java·架构·hibernate
阿甘知识库31 分钟前
宝塔面板跨服务器数据同步教程:双机备份零停机
android·运维·服务器·备份·同步·宝塔面板·建站
元争栈道1 小时前
webview+H5来实现的android短视频(短剧)音视频播放依赖控件资源
android·音视频
星河梦瑾1 小时前
SpringBoot相关漏洞学习资料
java·经验分享·spring boot·安全
黄名富1 小时前
Redis 附加功能(二)— 自动过期、流水线与事务及Lua脚本
java·数据库·redis·lua
love静思冥想1 小时前
JMeter 使用详解
java·jmeter
言、雲1 小时前
从tryLock()源码来出发,解析Redisson的重试机制和看门狗机制
java·开发语言·数据库
TT哇1 小时前
【数据结构练习题】链表与LinkedList
java·数据结构·链表