等待唤醒机制和阻塞队列

news/2024/10/4 19:27:33/文章来源:https://blog.csdn.net/2202_76097976/article/details/142148922

在这里插入图片描述

 

1. 等待唤醒机制

由于线程的随机调度,可能会出现“线程饿死”的问题:也就是一个线程加锁执行,然后解锁,其他线程抢不到,一直是这个线程在重复操作

void wait()

当前线程等待,直到被其他线程唤醒

void notify()

随机唤醒单个线程

void notifyAll()

唤醒所有线程

等待(wait):当一个线程执行到某个对象的wait()方法时,它会释放当前持有的锁(如果有的话),并进入等待状态。此时,线程不再参与CPU的调度,直到其他线程调用同一对象的notify()或notifyAll()方法将其唤醒,类似的,wait() 方法也可以传入一个参数表示等待的时间,不加参数就会一直等

唤醒(notify/notifyAll):

notify: 唤醒在该对象监视器上等待的某个线程,如果有多个线程在等待,那么具体唤醒哪一个是随机的

notifyAll: 唤醒在该对象监视器上等待的所有线程

1.1. wait

上面的方法是Object提供的方法,所以任意的Object对象都可以调用,下面来演示一下:

public class ThreadDemo14 {public static void main(String[] args) throws InterruptedException {Object obj = new Object();System.out.println("wait前");obj.wait();System.out.println("wait后");}
}

结果抛出了一个异常:非法的锁状态异常,也就是调用wait的时候,当前锁的状态是非法的

这是因为,在wait方法中,会先解锁然后再等待,所以要使用wait,就要先加个锁,阻塞等待就是把自己的锁释放掉再等待,不然一直拿着锁等待,其他线程就没机会了


把wait操作写在synchronized方法里就可以了,运行之后main线程就一直等待中,在jconsole中看到的也是waiting的状态

注意:wait操作进行解锁和阻塞等待是同时执行的(打包原子),如果不是同时执行就可能刚解锁就被其他线程抢占了,然后进行了唤醒操作,这时原来的线程再去等待,已经错过了唤醒操作,就会一直等

wait执行的操作:1. 释放锁并进入阻塞等待,准备接收唤醒通知 2. 收到通知后唤醒,并重新尝试获得锁

1.2. notify

接下来再看一下notify方法:

public class ThreadDemo15 {private static Object lock = new Object();public static void main(String[] args) {Thread t1 = new Thread(()->{synchronized (lock){System.out.println("t1 wait 前");try {lock.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("t1 wait 后");}});Thread t2 = new Thread(()->{synchronized (lock){System.out.println("t2 notify 前");Scanner sc = new Scanner(System.in);sc.next();//这里的输入主要是构造阻塞lock.notify();System.out.println("t2 notify 后");}});}
}

然后就会发现又出错了,还是之前的错误,notify也需要先加锁才可以

把之前的notify也加进synchornized就可以了,并且还需要确保是同一把锁

调用wait方法的线程会释放其持有的锁,被唤醒的线程在执行之前,必须重新获取被释放的锁

public class Cook extends Thread {@Overridepublic void run() {while (true) {synchronized (Desk.lock) {if (Desk.count == 0) {break;} else {if (Desk.foodFlag == 0) {try {Desk.lock.wait();//厨师等待} catch (InterruptedException e) {e.printStackTrace();}} else {Desk.count--;System.out.println("还能再吃" + Desk.count + "碗");Desk.lock.notifyAll();//唤醒所有线程Desk.foodFlag = 0;}}}}}
}
public class Foodie extends Thread {@Overridepublic void run() {while (true) {synchronized (Desk.lock) {if (Desk.count == 0) {break;} else {if (Desk.foodFlag == 1) {try {Desk.lock.wait();} catch (InterruptedException e) {e.printStackTrace();}} else {System.out.println("已经做好了");Desk.foodFlag = 1;Desk.lock.notifyAll();}}}}}
}
public class Desk {public static int foodFlag = 0;public static int count = 10;//锁对象public static Object lock = new Object();
}

这里实现的功能就是,厨师做好食物放在桌子上,美食家开始品尝,如果桌子上没有食物,美食家就等待,有的话,厨师进行等待

sleep() 和 wait() 的区别:

这两个方法看起来都是让线程等待,但是是有本质区别的,使用wait的目的是为了提前唤醒,sleep就是固定时间的阻塞,不涉及唤醒,虽然之前说的Interrupt可以使sleep提前醒来,但是Interrupt是终止线程,并不是唤醒,wait必须和锁一起使用,wait会先释放锁再等待,sleep和锁无关,不加锁sleep可以正常使用,加上锁sleep不会释放锁,抱着锁一起睡,其他线程无法拿到锁

在刚开始提到过,如果有多个线程都在同一个对象上wait,那么唤醒哪一个线程是随机的:

public class ThreadDemo16 {private static Object lock = new Object();public static void main(String[] args) {Thread t1 = new Thread(() -> {synchronized (lock) {System.out.println("t1 wait 前");try {lock.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("t1 wait 后");}});Thread t2 = new Thread(() -> {synchronized (lock) {System.out.println("t2 wait 前");try {lock.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("t2 wait 后");}});Thread t3 = new Thread(() -> {synchronized (lock) {System.out.println("t3 wait 前");try {lock.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("t wait 后");}});Thread t4 = new Thread(() -> {synchronized (lock) {System.out.println("t4 notify 前");Scanner sc = new Scanner(System.in);sc.next();lock.notify();System.out.println("t4 notify 后");}});t1.start();t2.start();t3.start();t4.start();}
}

这次只是t1被唤醒了

还可以使用notifyAll,把全部的线程都唤醒

2. 阻塞队列

2.1. 阻塞队列的使用

阻塞队列是一种特殊的队列,相比于普通的队列,它支持两个额外的操作:当队列为空时,获取元素的操作会被阻塞,直到队列中有元素可用;当队列已满时,插入元素的操作会被阻塞,直到队列中有空间可以插入新元素。

当阻塞队列满的时候,线程就会进入阻塞状态:

public class ThreadDemo19 {public static void main(String[] args) throws InterruptedException {BlockingDeque<Integer> blockingDeque = new LinkedBlockingDeque<>(3);blockingDeque.put(1);System.out.println("添加成功");blockingDeque.put(2);System.out.println("添加成功");blockingDeque.put(3);System.out.println("添加成功");blockingDeque.put(4);System.out.println("添加成功");}
}

同时,当阻塞队列中没有元素时,再想要往外出队,线程也会进入阻塞状态

public class ThreadDemo20 {public static void main(String[] args) throws InterruptedException {BlockingDeque<Integer> blockingDeque = new LinkedBlockingDeque<>(20);blockingDeque.put(1);System.out.println("添加成功");blockingDeque.put(2);System.out.println("添加成功");blockingDeque.take();System.out.println("take成功");blockingDeque.take();System.out.println("take成功");blockingDeque.take();System.out.println("take成功");}
}

2.2. 实现阻塞队列

根据阻塞队列的特性,可以尝试来自己手动实现一下

可以采用数组来模拟实现:

public class MyBlockingDeque {private String[] data = null;private int head = 0;private int tail = 0;private int size = 0;public MyBlockingDeque(int capacity) {data = new String[capacity];}
}

接下来是入队列的操作:

public void put(String s) throws InterruptedException {synchronized (this) {while (size == data.length) {this.wait();}data[tail] = s;tail++;if (tail >= data.length) {tail = 0;}size++;this.notify();}
}

由于设计到变量的修改,所以要加上锁,这里调用wait和notify来模拟阻塞场景,并且需要注意wait要使用while循环,如果说被Interrupted打断了,那么就会出现不可预料的错误

出队列也是相同的道理:

public String take() throws InterruptedException {
String ret = "";
synchronized (this) {while (size == 0) {this.wait();}ret = data[head];head++;if (head >= data.length) {head = 0;}size--;this.notify();
}
return ret;
}

3. 生产者消费者模型

生产者消费者模型是一种经典的多线程同步模型,用于解决生产者和消费者之间的协作问题。在这个模型中,生产者负责生产数据并将其放入缓冲区,消费者负责从缓冲区中取出数据并进行处理。生产者和消费者之间通过缓冲区进行通信,彼此之间不需要直接交互。这样可以降低生产者和消费者之间的耦合度,提高系统的可维护性和可扩展性。

而阻塞队列可以当做上面的缓冲区:

public class ThreadDemo21 {public static void main(String[] args) {BlockingDeque<Integer> blockingDeque = new LinkedBlockingDeque<>(100);Thread t1 = new Thread(()->{int i = 1;while (true){try {blockingDeque.put(i);System.out.println("生产元素:" + i);i++;Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});Thread t2 = new Thread(()->{while (true){try {int i = blockingDeque.take();System.out.println("消费元素:" + i);} catch (InterruptedException e) {throw new RuntimeException(e);}}});t1.start();t2.start();}
}

如果说把sleep的操作放到线程2会怎么样?

线程一瞬间就把阻塞队列沾满了,后面还是一个线程生产,一个线程消费,虽然打印出来的有偏差

生产者和消费者之间通过缓冲区进行通信,彼此之间不需要直接交互。这样可以降低生产者和消费者之间的耦合度,提高系统的可维护性和可扩展性。 

在这里插入图片描述

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.ldbm.cn/p/441188.html

如若内容造成侵权/违法违规/事实不符,请联系编程新知网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

STM32F407VET6 学习笔记2:定时器、串口、自定义串口打印函数

今日继续学习使用嘉立创购买的 立创梁山派天空星&#xff0c;芯片是 STM32F407VET6 因为已经有学习基础了&#xff0c;所以学习进度十分快&#xff0c;这次也是直接一块学习配置定时器与串口了&#xff0c;文章也愈来愈对基础的解释越来越少了...... 文章提供测试代码讲解、完…

计算机毕业设计选题推荐-项目评审系统-Java/Python项目实战

✨作者主页&#xff1a;IT毕设梦工厂✨ 个人简介&#xff1a;曾从事计算机专业培训教学&#xff0c;擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Py…

python程序使用nohup后台执行不能实时输出到定向文件的解决方法

问题描述&#xff1a;使用nohup命令后台执行python&#xff0c;但python中print方法打印结果不能实时输出到nohup后台定向文件&#xff0c;只能在程序结束时一次性输出。典型问题样例&#xff1a;在python中使用了os.system(command)方法&#xff0c;command命令打印的结果可以…

快速入门游戏领域,开发游戏需要哪些技术?

在这个充满创意和技术的时代&#xff0c;游戏行业成为众多创新人才追求梦想的热土。对于准备踏入这个充满挑战与机遇的领域的新人来说&#xff0c;了解游戏开发流程是至关重要的。 游戏市场蓬勃发展&#xff0c;游戏行业未来行情可观&#xff0c;在这个充满创意和技术的时代&a…

Open CASCADE学习|通过指定点的曲线

在OpenCASCADE中&#xff0c;如果你想通过一系列指定的点来创建一条曲线&#xff0c;你可以使用Geom2dAPI_Interpolate类来实现二维曲线的插值&#xff0c;或者使用GeomAPI_Interpolate类来实现三维曲线的插值。这些类允许你定义一条B样条曲线&#xff0c;这条曲线将精确地通过…

说说精益生产管理咨询公司排名的那些事

面对市场上琳琅满目的精益生产管理咨询公司&#xff0c;企业如何做出明智选择&#xff0c;避免陷入“坑”中&#xff0c;成为了一个值得探讨的话题。本文将从多个维度出发&#xff0c;为大家揭晓精益生产管理咨询公司排名的那些事&#xff0c;助您找到最适合的合作伙伴。 一、认…

三种方式可以将彩色图像转成灰度图对比

有三种方式可以将彩色图像转成灰度图 1、直接imread(“1.jpg” , 0)&#xff1b;直接读取灰度图像 2、读取彩色图像然后 灰度 0.299 * 红色 0.587 * 绿色 0.114 * 蓝色进行转换 3、调用cvtColor函数cvtColor(srcImg, imgShow, COLOR_BGR2GRAY); 通过测试来对比三者的区别&…

ctf.show靶场ssrf攻略

前言 欢迎来到我的博客 个人主页:北岭敲键盘的荒漠猫-CSDN博客 web351 解析:post传入url参数他就会访问。 解法: hackbar传入url参数写入https://127.0.0.1/flag.php web352 解析:post传入url参数&#xff0c;不能是127.0.0.1和localhost 解法:缩写127.1传入 web353 解析…

QT设置闹钟超时播报

头文件 #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include<QTimerEvent> #include<QTime> #include<QTextToSpeech>QT_BEGIN_NAMESPACE namespace Ui { class Widget; } QT_END_NAMESPACEclass Widget : public QWidget {Q_OBJECTpublic…

党务政务服务|基于SprinBoot+vue的党务政务服务热线系统(源码+数据库+文档)

党务政务服务热线系统 目录 基于SprinBootvue的党务政务服务热线系统 一、前言 二、系统设计 三、系统功能设计 系统功能实现 管理员功能模块 管理员功能模块 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八、源码获取&#xff1a; 博…

使用QT界面运行roslaunch,roslaunch,roscore等

QT通过界面运行rosrun,roslaunch,roscore等 QT 运行roslaunch加入ui界面修改cmakelist运行 使用qt界面运行rosrun&#xff0c;roscore,roslaunch等方法一方法二方法三 QT 运行roslaunch 首先需要使用QT安装好ROS插件&#xff0c;并且配置好环境&#xff0c;这个在之前的文章已…

webctf

熟悉robots.txt协议&#xff0c;可能存在一些敏感信息(sql在登录时候的万能密码a’ or true#)熟悉phps文件&#xff0c;phps文件就是 php 的源代码文件&#xff0c;通常用于提供给用户&#xff08;访问者&#xff09;查看 php 代码&#xff0c;因为用户无法直接通过 Web 浏览器…

所有即将登陆iPhone 16的Apple智能功能以及预期发布时间

苹果即将在9月9日的“Glowtime”&#xff08;闪耀时刻&#xff09;发布会上揭示和&#xff0c;这是本年度最值得期待的iPhone。 据悉&#xff0c;今年的iPhone将推出更大的屏幕、更快的芯片、更好的摄像头、新的颜色以及更多的内部升级。但是&#xff0c;除了这些硬件提升外&a…

Kubernetes上安装Metallb和Ingress并部署应用程序

视频和代码仓库 视频教程地址&#xff1a;https://www.bilibili.com/video/BV1QV4rebEb8 代码仓库地址&#xff1a;https://github.com/xiaohh-me/kubernetes-yaml 网络规划 之前已经写了几篇安装Kubernetes文章&#xff0c;这次来讲讲在Kubernetes上安装Ingress&#xff0c…

Qt+FFmpeg开发视频播放器笔记(三):音视频流解析封装

音频解析 音频解码是指将压缩的音频数据转换为可以再生的PCM(脉冲编码调制)数据的过程。 FFmpeg音频解码的基本步骤如下: 初始化FFmpeg解码器(4.0版本后可省略): 调用av_register_all()初始化编解码器。 调用avcodec_register_all()注册所有编解码器。 打开输入的音频流:…

vue3数据持久化方案:pinia-plugin-persistedstate源码浅析

概述 Pinia是vue3的官方推荐用于数据共享的库,但是Pinia🍍中的数据是存在于浏览器的内存中,当浏览器刷新后,这些数据就会消失。因此我们需要对数据做持久化存储,这个时候就需要用到pinia-plugin-persistedstate。 pinia-plugin-persistedstate本质上利用浏览器持久化存…

智慧平台赋能政务管理,声通科技助力政务管理智能化

在智能时代的大潮中&#xff0c;政务管理也在不断寻求创新与突破&#xff0c;在这方面&#xff0c;涌现出了很多优秀的公司。比如声通科技的子公司西安金讯数智信息技术有限公司&#xff0c;就在AI政务热线领域有很多创新成果&#xff0c;为政务管理的智能化升级提供了新思路。…

杰发科技Bootloader(3)—— 基于7801的APP切到Boot

为了方便在APP中跳转到Boot重新进行升级&#xff0c;有两种办法&#xff0c;7840同样可以使用。 1. 调用reset接口进行复位&#xff0c;复位后会先进Boot&#xff0c;再自动跳转到App。 NVIC_SystemReset(); 2. 直接使用跳转指令&#xff0c;参考Boot跳转到App代码&#xff0…

将小写字母转换为大写字母(c 语言)

2.我们第一步输入字符串&#xff0c;第二步进行筛选将字符串中所以下标为奇数位置上的字母转换成大写&#xff0c;如果该位置不是字母&#xff0c;则不转换。 #include <stdio.h> #include <string.h> void fun( char *ss ) {int i 0;while (*ss ! \0){if (i % 2…

AI菜鸟向前飞 — LangGraph系列之一:深入浅出解读Graph(一)

前言 LangGraph是一个使用 LLM 和 LangChain 构建有状态多参与者应用程序的库。 LangChain 允许您使用 LCEL&#xff08;LangChain 表达式语言&#xff09;构建链 AI菜鸟向前飞 — LangChain系列之六 - 深入浅出LCEL与Chain(上篇) AI菜鸟向前飞 — LangChain系列之七 - 深入…