wait和notify实现线程之间的通信

简介: 线程是并发并行的执行,表现出来是线程随机执行,但是我们在实际应用中对线程的执行顺序是有要求的,这就需要用到线程通信。

🍊一. 为什么需要线程通信

线程是并发并行的执行,表现出来是线程随机执行,但是我们在实际应用中对线程的执行顺序是有要求的,这就需要用到线程通信。


🍖线程通信为什么不使用优先级来来解决线程的运行顺序?


总的优先级是由线程pcb中的优先级信息和线程等待时间共同决定的,所以一般开发中不会依赖优先级来表示线程的执行顺序


🍖看下面这样的一个场景:面包房的例子来描述生产者消费者模型


有一个面包房,里面有面包师傅和顾客,对应我们的生产者和消费者,而面包房有一个库存用来存储面包,当库存满了之后就不在生产,同时消费者也在购买面包,当库存面包卖完了之后,消费者必须等待新的面包生产出来才能继续购买


分析:对于何时停止生产何时停止消费就需要应用到线程通信来准确的传达生产和消费信息


🍉二. wait和notify方法

🍃wait():让当前线程持有的对象锁释放并等待

🍃wait(long timeout):对应的参数是线程等待的时间

🍃notify():唤醒使用同一个对象调用wait进入等待的线程,重新竞争对象锁

🍃notifyAll():如果有多个线程等待,notifyAll是全部唤醒 ,notify是随机唤醒一个


👁‍🗨️注意:


🍂这几个方法都属于Object类中的方法

🍂必须使用在synchronized同步代码块/同步方法中

🍂哪个对象加锁,就是用哪个对象wait,notify

🍂调用notify后不是立即唤醒,而是等synchronized结束以后,才唤醒


🌴1. wait()方法

🍖调用wait方法后:


🍁使执行当前代码的线程进行等待(线程放在等待队列)

🍁释放当前的锁

🍁满足一定条件时被唤醒,重新尝试获取锁


🍖wait等待结束的条件:


🍃其他线程调用该对象的notify方法

🍃wait等待时间超时(timeout参数来指定等待时间)

🍃其他线程调用interrupted方法,导致wait抛出InterruptedException异常


🌾2. notify()方法

当使用wait不带参数的方法时,唤醒线程等待就需要使用notify方法


🍀这个方法是唤醒那些等待该对象的对象锁的线程,使他们可以重新获取该对象的对象锁

🍀如果有多个线程等待,则由线程调度器随机挑选出一个呈wait 状态的线程(不存在先来后到)

🍀在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁


🌵3. notifyAll()方法

该方法和notify()方法作用一样,只是唤醒的时候,将所有等待的线程都唤醒

notify()方法只是随机唤醒一个线程


🍏三. 使用wait和notify实现面包房业务

前提说明:


有2个面包师傅,面包师傅一次可以做出两个面包

仓库可以存储100个面包

有10个消费者,每个消费者一次购买一个面包


👁‍🗨️注意:


消费和生产是同时并发并行进行的,不是一次生产一次消费


👁‍🗨️实现代码:

public class Bakery {
    private static int total;//库存
    public static void main(String[] args) {
        Producer producer = new Producer();
        for(int i = 0;i < 2;i++){
            new Thread(producer,"面包师傅-"+(i-1)).start();
        }
        Consumer consumer = new Consumer();
        for(int i = 0;i < 10;i++){
            new Thread(consumer,"消费者-"+(i-1)).start();
        }
    }
    private static class Producer implements Runnable{
        private int num = 3; //生产者每次生产三个面包
        @Override
        public void run() {
            try {
                while(true){ //一直生产
                    synchronized (Bakery.class){
                        while((total+num)>100){ //仓库满了,生产者等待
                            Bakery.class.wait();
                        }
                        //等待解除
                        total += num;
                        System.out.println(Thread.currentThread().getName()+"生产面包,库存:"+total);
                        Thread.sleep(500);
                        Bakery.class.notifyAll(); //唤醒生产
                    }
                    Thread.sleep(500);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    private static class Consumer implements Runnable{
        private int num = 1; //消费者每次消费1个面包
        @Override
        public void run() {
            try {
                while(true){ //一直消费
                    synchronized (Bakery.class){
                        while((total-num)<0){ //仓库空了,消费者等待
                            Bakery.class.wait();
                        }
                        //解除消费者等待
                        total -= num;
                        System.out.println(Thread.currentThread().getName()+"消费面包,库存:"+total);
                        Thread.sleep(500);
                        Bakery.class.notifyAll(); //唤醒消费
                    }
                    Thread.sleep(500);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

部分打印结果:

微信图片_20221029144030.jpg


🍋四. 阻塞队列

阻塞队列是一个特殊的队列,也遵循“先进先出”的原则,它是线程安全的队列结构


特性:典型的生产者消费者模型,一般用于做任务的解耦和消峰


🍂队列满的时候,入队列就堵塞等待(生产),直到有其他线程从队列中取走元素

🍂队列空的时候,出队列就堵塞等待(消费),直到有其他线程往队列中插入元素


🌴1. 生产者消费者模型

生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题


生产者和消费者彼此之间不直接通信,而通过阻塞队列来进行通信,所以生产者生产完数据之后等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取


阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力

阻塞队列也能使生产者和消费者之间解耦


上述面包房业务的实现就是生产者消费者模型的一个实例


🌾2. 标准库中的阻塞队列

在 Java 标准库中内置了阻塞队列, 如果我们需要在一些程序中使用阻塞队列, 直接使用标准库中的即可


🍁BlockingQueue 是一个接口. 真正实现的类是 LinkedBlockingQueue

🍁put 方法用于阻塞式的入队列, take 用于阻塞式的出队列

🍁BlockingQueue 也有 offer, poll, peek 等方法, 但是这些方法不带有阻塞特性

BlockingDeque<String> queue = new LinkedBlockingDeque<>();
        queue.put("hello");
        //如果队列为空,直接出出队列就会阻塞
        String ret = queue.take();
        System.out.println(ret);

 

🌵3. 阻塞队列的模拟实现

这里使用数组实现一个循环队列来模拟阻塞队列


🍃当队列为空的时候,就不能取元素了,就进入wait等待,当有元素存放时,唤醒

🍃当队列为满的时候,就不能存元素了,就进入wait等待,当铀元素取出时,唤醒


👁‍🗨️实现代码:

public class MyBlockingQueue {
    //使用数组实现一个循环队列,队列里面存放的是线程要执行的任务
    private Runnable[] tasks;
    //队列中任务的数量,根据数量来判断是否可以存取
    private int count;
    private int putIndex; //存放任务位置
    private int takeIndex; //取出任务位置
    //有参的构造方法,表示队列容量
    public MyBlockingQueue(int size){
        tasks = new Runnable[size];
    }
    //存任务
    public void put(Runnable task){
        try {
            synchronized (MyBlockingQueue.class){
                //如果队列容量满了,则存任务等待
                while(count == tasks.length){
                    MyBlockingQueue.class.wait();
                }
                tasks[putIndex] = task; //将任务放入数组
                putIndex = (putIndex+1) % tasks.length; //更新存任务位置
                count++; //更新存放数量
                MyBlockingQueue.class.notifyAll(); //唤醒取任务
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    //取任务
    public Runnable take(){
        try {
            synchronized (MyBlockingQueue.class){
                //如果队列任务为空,则取任务等待
                while(count==0){
                    MyBlockingQueue.class.wait();
                }
                //取任务
                Runnable task = tasks[takeIndex];
                takeIndex = (takeIndex+1) % tasks.length; //更新取任务位置
                count--; //更新存放数量
                MyBlockingQueue.class.notifyAll(); //唤醒存任务
                return task;
            }
        } catch (InterruptedException e) {
           throw new RuntimeException("存放任务出错",e);
        }
    }
}


🍅五. wait和sleep的区别(面试题)

相同点:

都可以让线程放弃执行一段时间


不同点:

☘️wait用于线程通信,让线程在等待队列中等待

☘️sleep让线程阻塞一段时间,阻塞在阻塞队列中

☘️wait需要搭配synchronized使用,sleep不用搭配

☘️wait是Object类的方法,sleep是Thread的静态方法


相关文章
|
10天前
|
Java 调度
[Java]线程生命周期与线程通信
本文详细探讨了线程生命周期与线程通信。文章首先分析了线程的五个基本状态及其转换过程,结合JDK1.8版本的特点进行了深入讲解。接着,通过多个实例介绍了线程通信的几种实现方式,包括使用`volatile`关键字、`Object`类的`wait()`和`notify()`方法、`CountDownLatch`、`ReentrantLock`结合`Condition`以及`LockSupport`等工具。全文旨在帮助读者理解线程管理的核心概念和技术细节。
25 1
[Java]线程生命周期与线程通信
|
11天前
|
安全 Java
Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧
【10月更文挑战第20天】Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧,包括避免在循环外调用wait()、优先使用notifyAll()、确保线程安全及处理InterruptedException等,帮助读者更好地掌握这些方法的应用。
11 1
|
11天前
|
Java
在Java多线程编程中,`wait()`和`notify()`方法的相遇如同一场奇妙的邂逅
在Java多线程编程中,`wait()`和`notify()`方法的相遇如同一场奇妙的邂逅。它们用于线程间通信,使线程能够协作完成任务。通过这些方法,生产者和消费者线程可以高效地管理共享资源,确保程序的有序运行。正确使用这些方法需要遵循同步规则,避免虚假唤醒等问题。示例代码展示了如何在生产者-消费者模型中使用`wait()`和`notify()`。
17 1
|
11天前
|
安全 Java 开发者
Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用
本文深入解析了Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用。通过示例代码展示了如何正确使用这些方法,并分享了最佳实践,帮助开发者避免常见陷阱,提高多线程程序的稳定性和效率。
22 1
|
27天前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
38 1
C++ 多线程之初识多线程
|
11天前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
12 3
|
11天前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
10 2
|
11天前
|
Java
Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
23 2
|
11天前
|
Java 开发者
Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点
【10月更文挑战第20天】Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点,重点解析为何实现Runnable接口更具灵活性、资源共享及易于管理的优势。
22 1
|
11天前
|
Java
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件成立时被唤醒,从而有效解决数据一致性和同步问题。本文通过对比其他通信机制,展示了 `wait()` 和 `notify()` 的优势,并通过生产者-消费者模型的示例代码,详细说明了其使用方法和重要性。
19 1