java使用wait和notify实现线程通信

 更新时间:2023年10月07日 10:40:53   作者:终有救赎  
这篇文章主要为大家详细介绍了java如何使用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();
            }
        }
    }
}

部分打印结果:

四. 阻塞队列

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

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

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

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

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的静态方法

以上就是java使用wait和notify实现线程通信的详细内容,更多关于java wait notify的资料请关注脚本之家其它相关文章!

相关文章

  • 又又叕出BUG啦!理智分析Java NIO的ByteBuffer到底有多难用

    又又叕出BUG啦!理智分析Java NIO的ByteBuffer到底有多难用

    网络数据的基本单位永远是byte,Java NIO提供ByteBuffer作为字节的容器,但该类过于复杂,有点难用.本篇文章就带大家简单了解一下 ,需要的朋友可以参考下
    2021-06-06
  • SpringBoot接收请求参数的四种方式总结

    SpringBoot接收请求参数的四种方式总结

    这篇文章主要给大家介绍了关于SpringBoot接收请求参数的四种方式,文中通过代码以及图文介绍的非常详细,对大家学习或者使用SpringBoot具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-09-09
  • Spring中@PropertySource和@Value注解详解

    Spring中@PropertySource和@Value注解详解

    这篇文章主要介绍了Spring中@PropertySource和@Value注解详解,@PropertySource注解可以方便和灵活的向Spring的环境容器(org.springframework.core.env.Environment Environment)中注入一些属性,这些属性可以在Bean中使用,需要的朋友可以参考下
    2023-11-11
  • Spring中的retry重试组件详解

    Spring中的retry重试组件详解

    这篇文章主要介绍了Spring中的retry重试组件详解,Retry重试组件是一个处理重试逻辑的工具,可以在出现异常或失败情况下自动进行重试操作,从而提高程序的稳定性和可靠性,需要的朋友可以参考下
    2023-10-10
  • ThreadLocal导致JVM内存泄漏原因探究

    ThreadLocal导致JVM内存泄漏原因探究

    ThreadLocal是JDK提供的线程本地变量机制,但若使用不当可能导致内存泄漏。正确的使用方式是在使用完后及时remove,或者使用弱引用等手段避免强引用导致的内存泄漏。在多线程编程中,合理使用ThreadLocal可以提高并发性能,但也需要注意其潜在的内存泄漏问题
    2023-04-04
  • Java 反射调用静态方法的简单实例

    Java 反射调用静态方法的简单实例

    下面小编就为大家带来一篇Java 反射调用静态方法的简单实例。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2016-06-06
  • shiro整合springboot前后端分离

    shiro整合springboot前后端分离

    这篇文章主要介绍了shiro整合springboot前后端分离,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-12-12
  • ElasticSearch6.2.3+head插件安装的方法步骤

    ElasticSearch6.2.3+head插件安装的方法步骤

    这篇文章主要介绍了ElasticSearch6.2.3+head插件安装的方法步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-02-02
  • 详解Java HashMap实现原理

    详解Java HashMap实现原理

    HashMap是基于哈希表的Map接口实现,提供了所有可选的映射操作,并允许使用null值和null建,不同步且不保证映射顺序。本文将记录一下研究HashMap实现原理。
    2017-01-01
  • java 逐行读取txt文本如何解决中文乱码

    java 逐行读取txt文本如何解决中文乱码

    在使用java读取txt文本中如含有中文,可能会出现乱码,很多初学者束手无策,本文将提供详细的解决方法
    2012-11-11

最新评论