首页 > 其他分享 >多线程系列(六) -等待和通知模型详解

多线程系列(六) -等待和通知模型详解

时间:2024-02-22 18:24:04浏览次数:31  
标签:Thread lock 模型 28 详解 线程 notify 多线程 wait

一、简介

在之前的线程系列文章中,我们介绍了synchronizedvolatile关键字,使用它能解决线程同步的问题,但是它们无法解决线程之间协调和通信的问题。

举个简单的例子,比如线程 A 负责将 int 型变量 i 值累加操作到 10000,然后通知线程 B 负责把结果打印出来。

这个怎么实现呢?其中一个最简单的办法就是,线程 B 不断的通过轮询方式while(i == 10000)检查是否满足条件,这样就可以实现了。

虽然这种方式可以实现需求,但是也带来了另一个问题:线程 B 中的while()操作不会释放 CPU 资源,会导致 CPU 一直在这个方法上做判断操作,极大的浪费 CPU 资源。

我们知道 CPU 资源是非常非常昂贵的,因为使用 CPU 资源不只是当前一个应用程序,还有其它许许多多的应用程序。如果把这些轮询的时间释放出来,给别的线程使用,更能显著提升应用程序的运行效率。比如,线程 A 操作完成之后,通知线程 B 进行后续的操作,线程 B 无需通过轮询检查的方式来完成线程之间的协调,这样是不是更好。

在 Java 的父类中,也就是Object类中,就有三个方法:wait()notify()notifyAll(),它们就可以实现线程之间的通信。

如果没有接触多线程,这些方法可能基本上使用不到。下面我们一起来看看它们的使用方式!

二、方法介绍

  • wait()

wait()方法,顾名思义,表示等待的意思,它的作用是:使执行当前代码的线程进入阻塞状态,将当前线程置入"预执行队列"中,并且wait()所在的代码处停止执行,直到接到通知或被中断。

不过有个前提,在调用wait()方法之前,线程必须获得该对象的锁,因此只能在synchronized修饰的同步方法/同步代码块中调用wait()方法;同时,wait()方法执行后,会立即释放获得的对象锁以便其它线程使用,当前线程被阻塞,进入等待状态

至于wait()为什么有阻塞的效果,其内部机制非常复杂,主要由 JVM 的 C 代码实现,大家了解就行。

  • notify()

notify()方法,顾名思义,表示通知的意思,它的作用是:让处于同一监视器下的等待线程被重新唤醒,如果有多个线程等待,那么随机挑选出一个等待的线程,对其发出通知notify(),并使它等待获取该对象的对象锁。

注意“等待获取该对象的对象锁”,这意味着即使收到了通知,等待的线程也不会马上获取对象锁,必须等待notify()方法的线程释放锁才可以。

调用环境和wait()一样,notify()也要在synchronized修饰的同步方法/同步代码块中调用。

  • notifyAll()

notifyAll()方法,顾名思义,也是表示通知的意思,它的作用是:让所有处于同一监视器下的等待线程被重新唤醒,notify()方法只会随机的唤醒一个线程,而使用notifyAll()方法将一次性全部唤醒。

通常来说,notifyAll()方法更安全,因为当我们的代码逻辑考虑不周的时候,使用notify()会导致只唤醒了一个线程,而其他线程可能永远等待下去醒不过来了。

调用环境和notify()一样,notifyAll()也要在synchronized修饰的同步方法/同步代码块中调用。

三个方法总结下来就是:

  • 1.wait()方法,使线程阻塞,进入等待状态
  • 2.notify()方法,唤醒处于等待的线程,如果有多个线程就随机从中取一个
  • 3.notifyAll()方法,唤醒所有处于等待的线程

2.1、wait/notify/notifyAll 使用介绍

通常wait()方法,一般与notify()或者notifyAll()搭配使用比较多。

下面我们看一个简单的示例。

public class MyThreadA extends Thread{

    private Object lock;

    public MyThreadA(Object lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        synchronized (lock){
            System.out.println(DateUtil.format(new Date()) + " 当前线程:" + Thread.currentThread().getName() + " wait begin");
            try {
                // 进入阻塞等待
                lock.wait();
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println(DateUtil.format(new Date()) + " 当前线程:" + Thread.currentThread().getName() + " wait end");
        }
    }
}
public class MyThreadB extends Thread{

    private Object lock;

    public MyThreadB(Object lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        synchronized (lock){
            System.out.println(DateUtil.format(new Date()) + " 当前线程:" + Thread.currentThread().getName() + " notify begin");
            // 唤醒其它等待线程
            lock.notify();
            System.out.println(DateUtil.format(new Date()) + " 当前线程:" + Thread.currentThread().getName() + " notify end");
        }
    }
}
public class MyThreadTest {

    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        MyThreadA threadA = new MyThreadA(lock);
        threadA.start();

        //过3秒再启动下一个线程
        Thread.sleep(3000);

        MyThreadB threadB = new MyThreadB(lock);
        threadB.start();
    }
}

运行服务,输出结果如下:

2023-09-28 16:42:19 当前线程:Thread-0 wait begin
2023-09-28 16:42:22 当前线程:Thread-1 notify begin
2023-09-28 16:42:22 当前线程:Thread-1 notify end
2023-09-28 16:42:22 当前线程:Thread-0 wait end

从日志上可以得出,threadA线程先启动,然后进入阻塞状态,过了 3 秒之后,再启动threadB线程,运行结束之后,通知threadA线程可以获取对象锁,最后执行完毕。

整个线程之间的协调和通信,大体就是这样的。

假如我们把threadA线程数量增加到 5 个,再来看看运行效果。

public class MyThreadTest {

    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        // 创建5个wait线程
        for (int i = 0; i < 5; i++) {
            MyThreadA threadA = new MyThreadA(lock);
            threadA.start();
        }

        //过3秒再启动下一个线程
        Thread.sleep(3000);

        MyThreadB threadB = new MyThreadB(lock);
        threadB.start();
    }
}

运行服务,输出结果如下:

2023-09-28 17:02:05 当前线程:Thread-0 wait begin
2023-09-28 17:02:05 当前线程:Thread-4 wait begin
2023-09-28 17:02:05 当前线程:Thread-3 wait begin
2023-09-28 17:02:05 当前线程:Thread-2 wait begin
2023-09-28 17:02:05 当前线程:Thread-1 wait begin
2023-09-28 17:02:08 当前线程:Thread-5 notify begin
2023-09-28 17:02:08 当前线程:Thread-5 notify end
2023-09-28 17:02:08 当前线程:Thread-0 wait end

从日志中,可以很清晰的看到,当多个线程处于等待状态时,调用notify()方法,只会唤醒其中一个等待的线程;同时服务无法关闭,因为剩下的 4 个线程一直处于阻塞状态

假如我们把MyThreadB类中的lock.notify()方法改成lock.notifyAll()方法,再看看效果怎样。

public class MyThreadB extends Thread{

    private Object lock;

    public MyThreadB(Object lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        synchronized (lock){
            System.out.println(DateUtil.format(new Date()) + " 当前线程:" + Thread.currentThread().getName() + " notify begin");
            // 唤醒所有等待的线程
            lock.notifyAll();
            System.out.println(DateUtil.format(new Date()) + " 当前线程:" + Thread.currentThread().getName() + " notify end");
        }
    }
}

运行服务,输出结果如下:

2023-09-28 17:18:13 当前线程:Thread-0 wait begin
2023-09-28 17:18:13 当前线程:Thread-4 wait begin
2023-09-28 17:18:13 当前线程:Thread-3 wait begin
2023-09-28 17:18:13 当前线程:Thread-2 wait begin
2023-09-28 17:18:13 当前线程:Thread-1 wait begin
2023-09-28 17:18:16 当前线程:Thread-5 notify begin
2023-09-28 17:18:16 当前线程:Thread-5 notify end
2023-09-28 17:18:16 当前线程:Thread-1 wait end
2023-09-28 17:18:16 当前线程:Thread-2 wait end
2023-09-28 17:18:16 当前线程:Thread-3 wait end
2023-09-28 17:18:16 当前线程:Thread-4 wait end
2023-09-28 17:18:16 当前线程:Thread-0 wait end

从日志上可以很清晰的看到,3 秒后所有处于等待的线程都被唤醒,并且服务运行结束。

2.2、wait 释放锁介绍

在多线程的编程中,任何时候都要关注锁,因为它对当前代码执行是否安全,发挥了重要的作用。

在上面我们提到,调用wait()方法,除了让线程进入阻塞,进入等待状态以外,还会释放锁。

我们可以看一个简单的示例就知道了。

public class MyThreadA1 extends Thread{

    private Object lock;

    public MyThreadA1(Object lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        synchronized (lock){
            System.out.println(DateUtil.format(new Date()) + " 当前线程:" + Thread.currentThread().getName() + " wait begin");
            try {
                // 进入阻塞等待
                lock.wait();
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println(DateUtil.format(new Date()) + " 当前线程:" + Thread.currentThread().getName() + " wait end");
        }
    }
}
public class MyThreadTest1 {

    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();

        // 创建两个调用wait的线程
        MyThreadA1 threadA1 = new MyThreadA1(lock);
        threadA1.start();

        MyThreadA1 threadA2 = new MyThreadA1(lock);
        threadA2.start();
    }
}

运行服务,输出结果如下:

2023-09-28 17:31:56 当前线程:Thread-0 wait begin
2023-09-28 17:31:56 当前线程:Thread-1 wait begin

从日志结果可以清晰的看出,两个线程中其中一个调用lock.wait()之后,进入了阻塞状态,同时把对象锁也释放掉了,另一个线程拿到锁并进入同步代码块内,所以看到两个线程都打印了wait begin

Thread类中也有一个sleep()方法可以让当前线程阻塞,但是它们之间是有区别的,sleep()方法不会让当前线程释放锁。

我们可以看一个简单的例子。

public class MyThreadA1 extends Thread{

    private Object lock;

    public MyThreadA1(Object lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        synchronized (lock){
            System.out.println(DateUtil.format(new Date()) + " 当前线程:" + Thread.currentThread().getName() + " sleep begin");
            try {
                // 进入阻塞等待
                Thread.sleep(100);
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println(DateUtil.format(new Date()) + " 当前线程:" + Thread.currentThread().getName() + " sleep end");
        }
    }
}
public class MyThreadTest1 {

    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();

        // 创建两个调用sleep的线程
        MyThreadA1 threadA1 = new MyThreadA1(lock);
        threadA1.start();

        MyThreadA1 threadA2 = new MyThreadA1(lock);
        threadA2.start();
    }
}

运行服务,输出结果如下:

2023-09-28 17:55:20 当前线程:Thread-0 sleep begin
2023-09-28 17:55:21 当前线程:Thread-0 sleep end
2023-09-28 17:55:21 当前线程:Thread-1 sleep begin
2023-09-28 17:55:21 当前线程:Thread-1 sleep end

从日志上看,线程没有交替执行,而是串性执行。

2.3、notify/notifyAll 不释放锁介绍

于此对应的还有notify()notifyAll(), 调用notify()或者notifyAll()方法当前线程是不会释放锁的,只有当同步方法/同步代码块执行完毕,才会释放锁。

同样的,我们可以看一个简单的示例。

public class MyThreadA2 extends Thread{

    private Object lock;

    public MyThreadA2(Object lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        synchronized (lock){
            System.out.println(DateUtil.format(new Date()) + " 当前线程:" + Thread.currentThread().getName() + " notify begin");
            // 唤醒其它等待线程
            lock.notify();
            System.out.println(DateUtil.format(new Date()) + " 当前线程:" + Thread.currentThread().getName() + " notify end");
        }
    }
}
public class MyThreadTest2 {

    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();

        // 创建两个调用notify()的线程
        MyThreadA2 threadA1 = new MyThreadA2(lock);
        threadA1.start();

        MyThreadA2 threadA2 = new MyThreadA2(lock);
        threadA2.start();
    }
}

运行服务,输出结果如下:

2023-09-28 18:11:36 当前线程:Thread-0 notify begin
2023-09-28 18:11:36 当前线程:Thread-0 notify end
2023-09-28 18:11:36 当前线程:Thread-1 notify begin
2023-09-28 18:11:36 当前线程:Thread-1 notify end

从日志结果可以清晰的看出,两个线程没有交替执行,而是串行执行。

2.4、IllegalMonitorStateException 异常介绍

虽然wait()notify()notifyAll()方法是在 Object 类中,理论上每个类都可以直接调用,但不是每个地方都可以随便调用,如果调用这三个方法,不在同步方法/同步代码块中,程序运行时会直接抛一次抛异常java.lang.IllegalMonitorStateException

下面我们看一个简单的示例就知道了。

public class MyThreadTest3 {

    public static void main(String[] args) throws Exception {
        Object lock = new Object();
        lock.wait();
    }
}

运行程序,直接抛异常。

Exception in thread "main" java.lang.IllegalMonitorStateException
	at java.lang.Object.wait(Native Method)
	at java.lang.Object.wait(Object.java:502)
	at com.example.thread.e3.MyThreadTest3.main(MyThreadTest3.java:19)

换成notify()notifyAll(),运行结果也是一样。

三、小结

本文主要围绕线程之间的协调和通信相关技术进行一些知识总结,使用Object类中的wait()notify()notifyAll()方法,可以实现线程之间的协调和通信,但是它们只有在synchronized修饰的同步方法/同步代码块才会生效。如果不在同步方法/同步代码块调用,会抛java.lang.IllegalMonitorStateException异常。

文章内容难免有所遗漏,欢迎网友留言指出!

四、参考

1、廖雪峰 - wait和notify介绍

2、五月的仓颉 - wait()和notify()/notifyAll()介绍

五、写到最后

最近无意间获得一份阿里大佬写的刷题笔记,一下子打通了我的任督二脉,进大厂原来没那么难。

链接地址:技术资料笔记

不会有人刷到这里还想白嫖吧?点赞对我真的非常重要!在线求赞。加个关注我会非常感激!

标签:Thread,lock,模型,28,详解,线程,notify,多线程,wait
From: https://www.cnblogs.com/dxflqm/p/18024698

相关文章

  • shiro 整合 spring 实战及源码详解
    序言前面我们学习了如下内容:5分钟入门shiro安全框架实战笔记shiro整合spring实战及源码详解相信大家对于shiro已经有了最基本的认识,这一节我们一起来学习写如何将shiro与spring进行整合。spring整合maven依赖<dependencies><dependency><group......
  • Qt 图例类QLegend详解
    概述在Qt绘制图表时,图例并不是由QChart类所管理的,而是交给单独的QLegend类。QLegend类负责图例的绘制(包括颜色、线型、字体等),它与图表类QChart的关系是attach和detach。实例参考官方实例:X:\Qt\Qt5.9.0\Examples\Qt-5.9\charts\legend运行效果:功能详解设置图例标......
  • 文本生成视频模型——sora
    目录简介训练过程将可视化数据转化为patch使用不同分辨率、持续时间及纵横比的视频数据的优势关键点参考openAi提供的技术文档:https://openai.com/research/video-generation-models-as-world-simulators简介Sora是一种通用的视觉数据模型,它可以生成跨越不同持续时间、纵横比......
  • 关于RestCloud iPaaS平台的板块详解
    当今的企业分工越来越细,上下游合作越来越紧密、各企业之间的业务系统需要相互协作完成业务、外部API依赖越来越多、同时企业系统运行在多个混合云环境及SaaS中,私有端大量业务系统与云端系统形成了错综复杂的集成关系,企业面临集成技术复杂多样、API管理混乱、故障定位困难、数据推......
  • Ollama —— 在本地启动并运行大语言模型
    Ollama(https://ollama.com/)是一款命令行工具,可在macOS、Linux、Windows上本地运行Llama2、CodeLlama、Gemma等其他模型。以我这里mac下使用为例,下载对应版本后,直接放入应用目录,然后命令行执行。Gemma有2B与7B两个版本,我这里是15款的MacBookPro,就用低的这个版......
  • 常见IO模型
    任何技术的发展都是经过不断的演变迭代的,同样IO模型的演变代表着人们在计算机世界对效率的追求,对不同场景的解决方案,从某种方面来说IO模型的演变也一定程度见证着互联网的发展,随着学习的不断深入,也需要对底层实现原理不断加强。接下来主要针对计算机网络、网络分层模型、网络协议......
  • LM Studio装载模型时提示:You have 1 uncategorized model files
    使用LMStudio载入已经下载的模型的时候报错: 当前本地模型路径是默认的models路径,模型文件在models下面。 解决办法:在models目录下新建目录:Publisher\Repository即: 将模型文件移动到Repository中,重启LMStudio即可。......
  • flink之核心抽象--Window窗口及窗口操作全面详解
    flink之核心抽象--Window窗口及窗口操作全面详解标签:flink 窗口 String val -- 元素 Long window1.Windows1.1.基本概念窗口是处理无限流的核心。窗口将流划分为固定大小的“桶”,方便程序员在上面应用各种计算。Window操作是流式数据处理的一种非常核心的抽象,......
  • 跨越千年医学对话:用AI技术解锁中医古籍知识,构建能够精准问答的智能语言模型,成就专业级
    跨越千年医学对话:用AI技术解锁中医古籍知识,构建能够精准问答的智能语言模型,成就专业级古籍解读助手(LLAMA)介绍:首先在Ziya-LLaMA-13B-V1基线模型的基础上加入中医教材、中医各类网站数据等语料库,训练出一个具有中医知识理解力的预训练语言模型(pre-trainedmodel),之后在此基础上通过......
  • 视频直播点播平台EasyDarwin基础功能详解
    EasyDarwin是一款基于云计算的视频直播、点播及录像管理解决方案,旨在为用户提供稳定、高效和用户友好的视频流媒体体验。它通过简化视频内容的存储、管理和分享过程,使用户能够轻松构建和管控自己的视频直播和点播系统。在功能方面,EasyDarwin展现出了卓越的核心能力。首先,它的直播......