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

线程(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&#xf…...

【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人)并保存,然后分别按学生成绩由高到低顺序输出学生姓名和成绩,成绩相同时,则按输…...

shell脚本--常见案例

1、自动备份文件或目录 2、批量重命名文件 3、查找并删除指定名称的文件: 4、批量删除文件 5、查找并替换文件内容 6、批量创建文件 7、创建文件夹并移动文件 8、在文件夹中查找文件...

(二)原型模式

原型的功能是将一个已经存在的对象作为源目标,其余对象都是通过这个源目标创建。发挥复制的作用就是原型模式的核心思想。 一、源型模式的定义 原型模式是指第二次创建对象可以通过复制已经存在的原型对象来实现,忽略对象创建过程中的其它细节。 📌 核心特点: 避免重复初…...

基于Docker Compose部署Java微服务项目

一. 创建根项目 根项目&#xff08;父项目&#xff09;主要用于依赖管理 一些需要注意的点&#xff1a; 打包方式需要为 pom<modules>里需要注册子模块不要引入maven的打包插件&#xff0c;否则打包时会出问题 <?xml version"1.0" encoding"UTF-8…...

Mobile ALOHA全身模仿学习

一、题目 Mobile ALOHA&#xff1a;通过低成本全身远程操作学习双手移动操作 传统模仿学习&#xff08;Imitation Learning&#xff09;缺点&#xff1a;聚焦与桌面操作&#xff0c;缺乏通用任务所需的移动性和灵活性 本论文优点&#xff1a;&#xff08;1&#xff09;在ALOHA…...

(一)单例模式

一、前言 单例模式属于六大创建型模式,即在软件设计过程中,主要关注创建对象的结果,并不关心创建对象的过程及细节。创建型设计模式将类对象的实例化过程进行抽象化接口设计,从而隐藏了类对象的实例是如何被创建的,封装了软件系统使用的具体对象类型。 六大创建型模式包括…...

如何配置一个sql server使得其它用户可以通过excel odbc获取数据

要让其他用户通过 Excel 使用 ODBC 连接到 SQL Server 获取数据&#xff0c;你需要完成以下配置步骤&#xff1a; ✅ 一、在 SQL Server 端配置&#xff08;服务器设置&#xff09; 1. 启用 TCP/IP 协议 打开 “SQL Server 配置管理器”。导航到&#xff1a;SQL Server 网络配…...

C#最佳实践:为何优先使用as或is而非强制转换

C#最佳实践&#xff1a;为何优先使用as或is而非强制转换 在 C# 的编程世界里&#xff0c;类型转换是我们经常会遇到的操作。就像在现实生活中&#xff0c;我们可能需要把不同形状的物品重新整理归类一样&#xff0c;在代码里&#xff0c;我们也常常需要将一个数据类型转换为另…...

初级程序员入门指南

初级程序员入门指南 在数字化浪潮中&#xff0c;编程已然成为极具价值的技能。对于渴望踏入程序员行列的新手而言&#xff0c;明晰入门路径与必备知识是开启征程的关键。本文将为初级程序员提供全面的入门指引。 一、明确学习方向 &#xff08;一&#xff09;编程语言抉择 编…...

【HTML】HTML 与 CSS 基础教程

作为 Java 工程师&#xff0c;掌握 HTML 和 CSS 也是需要的&#xff0c;它能让你高效与前端团队协作、调试页面元素&#xff0c;甚至独立完成简单页面开发。本文将用最简洁的方式带你掌握核心概念。 一、HTML&#xff0c;网页骨架搭建 核心概念&#xff1a;HTML通过标签定义内…...

Monorepo架构: 项目管理模式对比与考量

关于 monorepo 相关概念及项目管理模式 在软件开发中&#xff0c;尤其是前端项目&#xff0c;我们会涉及到不同的项目管理模式&#xff0c;这里先介绍几个重要的概念“monorepo”是当前较为热门的一种项目管理方式&#xff0c;虽然很多人可能听说过&#xff0c;但可能在实际项…...