线程(Thread)的三种等待唤醒机制详解
1、为什么需要线程的等待和唤醒
线程的等待唤醒机制是一种经典的“生产者和消费者”模型。例如食品加工厂,食品加工人员和原料补给人员,在有充足原料时,补给人员是在等待,等到原料不够时,食品加工人员通知补给人员(唤醒)。在我们开发诸如此类需求的时候两个线程之间协调等待和唤醒还是很有必要的。那咱们看看都有那些方式实现线程等待和唤醒机制。
2、线程等待和唤醒机制方式
实现线程等待和唤醒机制主要有3中方式分别是:
- 使用 Object 中的 wait() 方法让线程等待,使用 Object 中的 notify() 方法唤醒线程
- 使用 JUC 包中 Condition 的 await() 方法让线程等待,使用 signal() 方法唤醒线程
- 使用LockSupport类的park()方法让线程等待,使用unpark()方法唤醒线程。
那这三种方式具体怎么使用,都有那些优缺点呢?我们通过实例代码演示。
3、线程等待和唤醒机制实例
3.1、使用 Object 中的 wait() 方法让线程等待,使用 Object 中的 notify() 方法唤醒线程
代码:
package com.lc.test02;import java.util.concurrent.TimeUnit;/*** @author liuchao* @date 2023/4/8*/
public class ThreadWaitOne {/*** 注意:必须使用同一把锁*/static Object lock = new Object();public static void main(String[] args) {/*** 线程1*/Thread t1 = new Thread(() -> {synchronized (lock) {System.out.println("进入" + Thread.currentThread().getName());try {lock.wait();} catch (InterruptedException e) {Thread.currentThread().interrupt();}System.out.println(Thread.currentThread().getName() + "被唤醒");}}, "t1");t1.start();try {TimeUnit.MILLISECONDS.sleep(200);} catch (InterruptedException e) {Thread.currentThread().interrupt();}/*** 线程2*/Thread t2 = new Thread(() -> {synchronized (lock) {System.out.println("进入" + Thread.currentThread().getName());lock.notify();System.out.println("唤醒通知已发");}}, "t2");t2.start();}
}
效果:
进入t1
进入t2
唤醒通知已发
t1被唤醒
总结:此种方式必须使用同一把锁并且必须包含在synchronized代码块中,如果未使用synchronized包裹,则会报错。
Exception in thread "t1" java.lang.IllegalMonitorStateException
at java.lang.Object.wait(Native Method)
at java.lang.Object.wait(Object.java:502)
at com.lc.test02.ThreadWaitOne.lambda$main$0(ThreadWaitOne.java:24)
at java.lang.Thread.run(Thread.java:750)
并且wait方法的调用必须要在notify/notifyAll的调用之前,否则线程将永远不会被唤醒。
3.2、使用 JUC 包中 Condition 的 await() 方法让线程等待,使用 signal() 方法唤醒线程
代码:
package com.lc.test02;import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;/*** @author liuchao* @date 2023/4/8*/
public class ThreadWaitTwo {/*** 必须是同一把锁*/static Lock lock = new ReentrantLock();static Condition condition = lock.newCondition();public static void main(String[] args) {/*** 线程1*/Thread t1 = new Thread(() -> {lock.lock();try {System.out.println("进入" + Thread.currentThread().getName());condition.await();} catch (InterruptedException e) {Thread.currentThread().interrupt();} finally {lock.unlock();}System.out.println(Thread.currentThread().getName() + "被唤醒");}, "t1");t1.start();try {TimeUnit.MILLISECONDS.sleep(200);} catch (InterruptedException e) {Thread.currentThread().interrupt();}/*** 线程2*/Thread t2 = new Thread(() -> {lock.lock();try {System.out.println("进入" + Thread.currentThread().getName());condition.signal();System.out.println("唤醒通知已发");} finally {lock.unlock();}}, "t2");t2.start();}
}
效果:
进入t1
进入t2
唤醒通知已发
t1被唤醒
总结:使用此中方式必须配合lock,代码必须被lock包裹,否则将报错
Exception in thread "t1" java.lang.IllegalMonitorStateException
at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:151)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1261)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.fullyRelease(AbstractQueuedSynchronizer.java:1723)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2036)
at com.lc.test02.ThreadWaitTwo.lambda$main$0(ThreadWaitTwo.java:28)
at java.lang.Thread.run(Thread.java:750)
并且await方法的调用必须在signal/signalAll方法调用之前,否则t1也是不会被唤醒
3.3、使用LockSupport类的park()方法让线程等待,使用unpark()方法唤醒线程。
LockSupport 类使用了一种名为 Permit (许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit),permit 只有两个值 1 和 0,默认是 0
- 阻塞
①、park()/park(Object blocker)
②、permit 默认是 0,所以一开始调用 park() 方法,当前线程就会阻塞,直到别的线程将当前线程的 permit 设置为 1 时,park 方法会被唤醒,然后会将 permit 再次设置为 0 并返回。
③、阻塞当前线程/阻塞传入的具体线程
- 唤醒
①、unpark(Thread thread)
②、调用 unpark(Thread thread) 方法后,就会将 thread 线程的许可 permit 设置为 1(注意多次调用 unpark 方法,不会累加,permit 值还是 1)会自动唤醒 thread 线程,即之前阻塞中的 LockSupport.park()方法会立即返回
③、唤醒处于阻塞状态的指定线程
代码:
package com.lc.test02;import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;/*** @author liuchao* @date 2023/4/8*/
public class ThreadWaitThree {public static void main(String[] args) {/*** 线程1*/Thread t1 = new Thread(() -> {System.out.println("进入" + Thread.currentThread().getName());LockSupport.park();System.out.println(Thread.currentThread().getName() + "被唤醒");}, "t1");t1.start();try {TimeUnit.MILLISECONDS.sleep(200);} catch (InterruptedException e) {Thread.currentThread().interrupt();}/*** 线程2*/Thread t2 = new Thread(() -> {System.out.println("进入" + Thread.currentThread().getName());LockSupport.unpark(t1);System.out.println("唤醒通知已发");}, "t2");t2.start();}
}
效果:
进入t1
进入t2
唤醒通知已发
t1被唤醒
结论:此种方式不需要增加同步机制,天生就是无锁机制实现线程等待和唤醒,并且因为是通过许可方式来唤醒线程的,所以许可是在等待(调用park()方法)前还是后是不影响的(park和unpark调用先后顺序我关),此种方式也是我们推荐使用的方式。当然,这种方式也是有缺点的,许可最多只有一个,如果等待多次(调用多次park()方法),线程也就永远不能唤醒了。
演示线程的唤醒和unpark先调用还是后调用无关:
package com.lc.test02;import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;/*** @author liuchao* @date 2023/4/8*/
public class ThreadWaitThree {public static void main(String[] args) {/*** 线程1*/Thread t1 = new Thread(() -> {try {//等待300毫秒,让t2先执行TimeUnit.MILLISECONDS.sleep(300);} catch (InterruptedException e) {Thread.currentThread().interrupt();}System.out.println("进入" + Thread.currentThread().getName());LockSupport.park();System.out.println(Thread.currentThread().getName() + "被唤醒");}, "t1");t1.start();/*** 线程2*/Thread t2 = new Thread(() -> {System.out.println("进入" + Thread.currentThread().getName());LockSupport.unpark(t1);System.out.println("唤醒通知已发");}, "t2");t2.start();}
}
效果:
进入t2
唤醒通知已发
进入t1
t1被唤醒
演示多次调用unpark+多次调用park导致线程不能被唤醒:
package com.lc.test02;import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;/*** @author liuchao* @date 2023/4/8*/
public class ThreadWaitThree {public static void main(String[] args) {/*** 线程1*/Thread t1 = new Thread(() -> {System.out.println("进入" + Thread.currentThread().getName());/*** 调用两次park方法*/LockSupport.park();LockSupport.park();System.out.println(Thread.currentThread().getName() + "被唤醒");}, "t1");t1.start();try {//等待300毫秒,让t2先执行TimeUnit.MILLISECONDS.sleep(300);} catch (InterruptedException e) {Thread.currentThread().interrupt();}/*** 线程2*/Thread t2 = new Thread(() -> {System.out.println("进入" + Thread.currentThread().getName());/*** 调用了两次park我们调用三次unpark 还是无法唤醒t1线程的*/LockSupport.unpark(t1);LockSupport.unpark(t1);LockSupport.unpark(t1);System.out.println("唤醒通知已发");}, "t2");t2.start();}
}

相关文章:
线程(Thread)的三种等待唤醒机制详解
1、为什么需要线程的等待和唤醒 线程的等待唤醒机制是一种经典的“生产者和消费者”模型。例如食品加工厂,食品加工人员和原料补给人员,在有充足原料时,补给人员是在等待,等到原料不够时,食品加工人员通知补给人员&am…...
从零学习python - 13模块的导入与使用(实现单例模式)
模块基础知识 # 项目 > 包 > 模块 > 变量\方法\类 # 在python中,模块是代码组织的一种方式,把功能相近的函数或类放到一个文件中,一个文件(.py)就是一个模块,模块名就是文件名去掉py后缀. # 好处:提高代码可复用性和可维护性,一个模块编写完成后,很方便在其他项目中导…...
国产SSD、内存卷哭国外大厂,三星宣布减产涨价在路上了
PC 圈有一句话是这么说的:论价格屠夫还得看国产品牌! 可不是嘛,国产长鑫、长江算是彻底将全球存储芯片市场搅局者这一「骂名」坐实了! 不说特别早期,前几年吧,普通单条 8G DDR4 内存都能卖到六七百元&…...
数据库管理-第六十六期 SQL Domain(20230413)
数据库管理 2023-04-13第六十六期 SQL Domain1 基本介绍2 Domain的表达式和条件3 语法4 语义5 示例总结第六十六期 SQL Domain 上一期一笔带过了部分Oracle 23c的新特性,这一期重点讲一下SQL Domain新特性。 【https://docs.oracle.com/en/database/oracle/oracle-…...
《Vue3实战》 第一章 nods/npm安装、配置
1、nods.js安装(Windows) 1.1、下载并安装node https://nodejs.org/en/ , 安装到d盘nodejs目录 1.2、配置环境变量 path配置 1.3、配置全局包存放目录和缓存目录 在根目录下创建node_global(全局包存放目录)和node_cache&…...
JAVA练习104-四数相加 II
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 目录 前言 一、题目-四数相加 II 1.题目描述 2.思路与代码 2.1 思路 2.2 代码 总结 前言 提示:这里可以添加本文要记录的大概内容: 4月10日练…...
【C++基础】引用(引用的概念;引用的特性;常引用;使用场景:做输出型参数、大对象传参、做输出型返回值、返回大对象的引用);引用和指针的区别)
六、引用 6.1 引用的概念 引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。(语法上) 格式:类型& 引用变量名(对象名) …...
Redis只用来做缓存?来认识一下它其他强大的能力吧。
当今互联网应用中,随着业务的发展,数据量越来越大,查询效率越来越高,对于时序数据的存储、查询和分析需求也越来越强烈,这时候 Redis 就成为了首选的方案之一。 Redis 提供了多种数据结构,如字符串、哈希表…...
【ES】数据同步集群
【ES】数据同步&集群3.数据同步3.1.思路分析3.1.1.同步调用3.1.2.异步通知3.1.3.监听binlog3.1.4.选择3.2.实现数据同步3.2.1.思路3.2.2.导入demo3.2.3.声明交换机、队列1)引入依赖2)声明队列交换机名称3)声明队列交换机3.2.4.发送MQ消息…...
37岁男子不愿熬夜,回乡养鸡每天准时下班,青山绿水中养鸡,直播间里卖鸡蛋...
37岁男子不愿熬夜,回乡养鸡每天准时下班,青山绿水中养鸡,直播间里卖鸡蛋。今天和大家分享一个创业案例,他叫胡铭浩,来自安徽省旌德县,今年37岁,曾做过车床操作工,开过婚纱摄影店&…...
深度学习和人工智能之间是什么样的关系?
深度学习与人工智能概念的潜在联系,我们依然借助维恩图来说明,如图4.1所示。 1、人工智能 “人工智能”这个概念新鲜时髦但又含混模糊,同时包罗万象。尽管如此,我们仍尝试对 人工智能进行定义:用一台机器处理来自其周围环境的信息,然后将这些…...
实战打靶集锦-016-lampiao
提示:本文记录了博主打靶过程中一次曲折的提权经历 文章1. 主机发现2. 端口扫描3. 服务枚举4. 服务探查4.1 80端口探查4.2 1898端口探查4.3 EXP搜索4.3.1 exploit/unix/webapp/drupal_coder_exec4.3.2 exploit/unix/webapp/drupal_drupalgeddon25. 提权5.1 系统信息…...
《Web前端应用开发》考试试卷(模拟题)
一、产品搜索页面 打开“考试文件夹”中的input.html,完成以下步骤: 注意:本题仅能在input.html的(1)为产品名称所在的div添加样式属性,使得产品名称保持在文本框的左边; (2…...
【react全家桶学习】react简介
react是什么? react是用于构建用户界面的JS库,是一个将数据渲染为HTML视图的开源JS库 谁开发的? 由Facebook开发,且开源 为什么要学? 原生JavaScript操作DOM繁琐、效率低 ( DOM-API操作 UI)使用JavaScript直接操作…...
此战成硕,我成功上岸西南交通大学了~~~
友友们,好久不见,很长时间没有更一个正式点的文章了! 是因为我在去年年底忙着准备初试,今年年初在准备复试,直到3月底拟录取后,终于可以写下这篇上岸贴,和大家分享一下考研至上岸的一个过程 文章…...
光耦继电器工作原理及优点概述
光耦继电器是一种电子元器件,也是固态继电器的一种,其主要作用是隔离输入与输出电路,用于保护或者控制电路的正常工作。 光耦继电器工作原理是利用光电转换器将外界信号转化为光信号,通过光纤传输到另一端,再由另一端的…...
【Mysql】mysql8.0.26解压包部署方式
版本背景: 操作系统:centos7.3 mysql版本:mysql-8.0.26-linux-glibc2.12-x86_64.tar 一、前期准备 1、检测操作系统自带安装的mysql和mariadb服务,如存在,需卸载 rpm -qa | grep mysql rpm -qa | grep mariadb 卸载…...
进销存管理系统能为企业带来哪些实际效益?
随着互联网的不断发展,如今的商业世界已经越来越向数字化转型。拥有一套完整的数字化的进销存管理能够极大地提升公司货物进出库存情况的效率和准确性,避免过程中出现不必要的错误和漏洞,从而帮助企业更加稳健地自我发展。那么,一…...
图片怎么转换成pdf格式?这几个方法帮你一键转换
现今电子书籍越来越受到欢迎,其中PDF格式也成为了一种常用的电子书籍格式。无论是工作还是学习,我们都可能会遇到需要将图片转换成PDF格式的情况,例如保存一些资料证明、公文公告、学习资料等。在这篇文章中,我们将为大家介绍三种…...
数据结构exp1_2学生成绩排序
目录 数据结构exp1_2学生成绩排序 程序设计 程序分析 数据结构exp1_2学生成绩排序 【问题描述】 对某班学生成绩排序。从键盘依次输入某班学生的姓名和成绩(一个班级人数最多不超过50人)并保存,然后分别按学生成绩由高到低顺序输出学生姓名和成绩,成绩相同时,则按输…...
Golang 面试经典题:map 的 key 可以是什么类型?哪些不可以?
Golang 面试经典题:map 的 key 可以是什么类型?哪些不可以? 在 Golang 的面试中,map 类型的使用是一个常见的考点,其中对 key 类型的合法性 是一道常被提及的基础却很容易被忽视的问题。本文将带你深入理解 Golang 中…...
Objective-C常用命名规范总结
【OC】常用命名规范总结 文章目录 【OC】常用命名规范总结1.类名(Class Name)2.协议名(Protocol Name)3.方法名(Method Name)4.属性名(Property Name)5.局部变量/实例变量(Local / Instance Variables&…...
微信小程序 - 手机震动
一、界面 <button type"primary" bindtap"shortVibrate">短震动</button> <button type"primary" bindtap"longVibrate">长震动</button> 二、js逻辑代码 注:文档 https://developers.weixin.qq…...
Linux云原生安全:零信任架构与机密计算
Linux云原生安全:零信任架构与机密计算 构建坚不可摧的云原生防御体系 引言:云原生安全的范式革命 随着云原生技术的普及,安全边界正在从传统的网络边界向工作负载内部转移。Gartner预测,到2025年,零信任架构将成为超…...
JUC笔记(上)-复习 涉及死锁 volatile synchronized CAS 原子操作
一、上下文切换 即使单核CPU也可以进行多线程执行代码,CPU会给每个线程分配CPU时间片来实现这个机制。时间片非常短,所以CPU会不断地切换线程执行,从而让我们感觉多个线程是同时执行的。时间片一般是十几毫秒(ms)。通过时间片分配算法执行。…...
selenium学习实战【Python爬虫】
selenium学习实战【Python爬虫】 文章目录 selenium学习实战【Python爬虫】一、声明二、学习目标三、安装依赖3.1 安装selenium库3.2 安装浏览器驱动3.2.1 查看Edge版本3.2.2 驱动安装 四、代码讲解4.1 配置浏览器4.2 加载更多4.3 寻找内容4.4 完整代码 五、报告文件爬取5.1 提…...
什么是Ansible Jinja2
理解 Ansible Jinja2 模板 Ansible 是一款功能强大的开源自动化工具,可让您无缝地管理和配置系统。Ansible 的一大亮点是它使用 Jinja2 模板,允许您根据变量数据动态生成文件、配置设置和脚本。本文将向您介绍 Ansible 中的 Jinja2 模板,并通…...
Redis的发布订阅模式与专业的 MQ(如 Kafka, RabbitMQ)相比,优缺点是什么?适用于哪些场景?
Redis 的发布订阅(Pub/Sub)模式与专业的 MQ(Message Queue)如 Kafka、RabbitMQ 进行比较,核心的权衡点在于:简单与速度 vs. 可靠与功能。 下面我们详细展开对比。 Redis Pub/Sub 的核心特点 它是一个发后…...
回溯算法学习
一、电话号码的字母组合 import java.util.ArrayList; import java.util.List;import javax.management.loading.PrivateClassLoader;public class letterCombinations {private static final String[] KEYPAD {"", //0"", //1"abc", //2"…...
提升移动端网页调试效率:WebDebugX 与常见工具组合实践
在日常移动端开发中,网页调试始终是一个高频但又极具挑战的环节。尤其在面对 iOS 与 Android 的混合技术栈、各种设备差异化行为时,开发者迫切需要一套高效、可靠且跨平台的调试方案。过去,我们或多或少使用过 Chrome DevTools、Remote Debug…...
