当前位置: 首页 > news >正文

等待唤醒机制和阻塞队列

在这里插入图片描述

 

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会怎么样?

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

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

在这里插入图片描述

 

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • Java List转Map
  • 【C++ Primer Plus习题】15.2
  • 如何使用树莓派构建 LoRa 网关?
  • 小说阅读网站登录注册搜索小说查看评论前后台管理计算机毕业设计/springboot/javaWEB/J2EE/MYSQL数据库/vue前后分离小程序
  • 通过ASCII码打印HelloWorld(花式打印HelloWorld)
  • post请求中有[]报400异常
  • 对游戏语音软件Oopz遭遇DDoS攻击后的一些建议
  • Element UI:初步探索 Vue.js 的高效 UI 框架
  • JavaScript变量
  • 2024年华为杯数学建模研赛 最全赛中助攻|选题建议+思路+代码+成品论文预定
  • 数据赋能(200)——开发:数据开发管理——影响因素、直接作用、主要特征
  • 高级算法设计与分析 学习笔记4 二叉查找树
  • 没钱才懂的道理,我推荐你读这4本书
  • MySQL之安装与基础知识
  • 【video clips 专栏 2.1 -- videopad 删除视频中间部分】
  • ES6指北【2】—— 箭头函数
  • [译] React v16.8: 含有Hooks的版本
  • ➹使用webpack配置多页面应用(MPA)
  • axios 和 cookie 的那些事
  • CSS 专业技巧
  • ES10 特性的完整指南
  • exif信息对照
  • Java 9 被无情抛弃,Java 8 直接升级到 Java 10!!
  • oldjun 检测网站的经验
  • PhantomJS 安装
  • SQLServer插入数据
  • ubuntu 下nginx安装 并支持https协议
  • vagrant 添加本地 box 安装 laravel homestead
  • vue 配置sass、scss全局变量
  • Vue源码解析(二)Vue的双向绑定讲解及实现
  • 番外篇1:在Windows环境下安装JDK
  • 基于 Babel 的 npm 包最小化设置
  • 技术:超级实用的电脑小技巧
  • 聚类分析——Kmeans
  • 聊聊hikari连接池的leakDetectionThreshold
  • 【运维趟坑回忆录 开篇】初入初创, 一脸懵
  • Linux权限管理(week1_day5)--技术流ken
  • 说说我为什么看好Spring Cloud Alibaba
  • ​补​充​经​纬​恒​润​一​面​
  • #pragma once与条件编译
  • #我与Java虚拟机的故事#连载03:面试过的百度,滴滴,快手都问了这些问题
  • (12)目标检测_SSD基于pytorch搭建代码
  • (LeetCode 49)Anagrams
  • (vue)el-tabs选中最后一项后更新数据后无法展开
  • (附源码)计算机毕业设计SSM疫情下的学生出入管理系统
  • (六)c52学习之旅-独立按键
  • (十六)一篇文章学会Java的常用API
  • (五)MySQL的备份及恢复
  • .NET Core 中插件式开发实现
  • .Net Core中的内存缓存实现——Redis及MemoryCache(2个可选)方案的实现
  • .net MVC中使用angularJs刷新页面数据列表
  • .NET 将混合了多个不同平台(Windows Mac Linux)的文件 目录的路径格式化成同一个平台下的路径
  • .NET 事件模型教程(二)
  • .NET的数据绑定
  • .NET牛人应该知道些什么(2):中级.NET开发人员