JUC(6) : LockSupport | 优雅的线程通信工具

news2024/11/20 0:45:26

一、前言

前文介绍了 CompletableFuture 和 线程池的几种对线程的管理方式后,本质上,通过这些工具,可以直接帮我们对线程进行很好的管理和运作,什么时间需要启动哪个线程,以及线程的执行顺序等。毕竟,线程间的通信技术是非常有价值的,如果仅仅是孤立的运行各个线程不产生任何协作,那多线程也就失去了更多利用价值。本文再一种 JDK 自带的线程等待唤醒机制--LockSupport,通过它的使用,可以学习更优雅的线程间通信技术,同时为后续的 AQS 的学习做好准备。

二、概述

2.1 LockSupport 介绍

LockSupport 是 java.util.concurrent.locks 包下的一个工具类。

LockSupport 可以类比于一个列车检票员的角色,每个进入的线程都得亮出自己的 “车票” ,如果有车票,那列车员就会在你的车票上剪出一个口子,这样你的车票就相当于销毁了,无法用这个票上其他的车,如果没有车票就拒之门外,无法上车,如果你想买票,它也可以让你买一张车票。

分析它的特点:

  • 1、如果有 “车票” ,就放你通过,同时你的 车票会销毁。
  • 2、如果没有车票,则阻塞,无法通信。
  • 3、车票最多只有一张。

LockSupport 是用于创建锁和其他同步类的基本线程的阻塞原语。

LockSupport 不支持构造(private 修饰),主要的作用是阻塞和唤醒线程,提供了一堆 static 方法,例如 park 和 unpark,用于停车和发车的动作。park 消耗一个车票,unpark 新增一个车票,车票最多只有一个。

主要有如上的静态方法,常用的 park 和 unpark 需要承成对使用,类比于 Object 的 wait 和 notify 为一对。

permit

LockSupport 的设计思路是为每个线程设置一个 permit,其实就是一个值。

这个值默认就是 0,有0和1两种值,0代表许可证不可用;1 代表许可证可用。

  • 调用 park 方法进行减一 扣除许可证,调用 unPark 方法进行加一,将许可证置为可用。
  • 当为 0 时,无论 park 多少次,都只能为0;
  • 当为 1 时,无论 unpark 多少次,最大只能为 1。

2.2 入门使用

 @Test
 public void testLockSupport() {
     Thread parkThread = new Thread(() -> {
         System.out.println("开始线程阻塞");
         LockSupport.park();
         System.out.println("结束线程阻塞");
     });
     parkThread.start();
     System.out.println("开始线程唤醒");
     LockSupport.unpark(parkThread);
     System.out.println("结束线程唤醒");
 }
复制代码

这串代码最终 parkThread 会在 unpark 线程后才会结束线程阻塞,这也说明了 LockSupport 可以对线程进行阻塞和唤醒的操作,阻塞时可以传入具体线程进行唤醒。

三、三种线程间通信

介绍三种对线程等待和唤醒的方法。

  • 1、使用 Object 种的 wait()方法让线程等待,使用 Object种的 notify()方法唤醒线程;
  • 2、使用 JUC 包种 Condition 的 await()方法让线程等待,使用 signal()方法唤醒线程;
  • 3、LockSupport 类可以阻塞当前线程以及唤醒指定被阻塞的线程。

3.1 Object 的wait 和 notify 方法实现线程等待和唤醒

使用该方法有两个问题:

  • 1)要使用 Object 的wait 和 notify,必须要用个对象锁,才能使用
  • 2)先 wait 后 notify 才 OK

3.1.1 无对象锁

 public class ThreadCommunication {
     public static void main(String[] args) {
         Resource resource = new Resource();
         new Thread(()->{
             for (int i = 0; i < 5; i++) {
                 resource.decr();
             }
         },"减一线程").start();
         new Thread(()->{
             for (int i = 0; i < 5; i++) {
                 resource.incr();
             }
         },"加一线程").start();
     }
 }
复制代码
 /**
  * 锁唤醒机制实现
  */
 // 资源类
 class Resource{
     // 加减对象
     private int number = 0;
     // 加法
     public  void incr(){
         try {
         while (number!=0){
             this.wait();
         }
         number++;
         System.out.println("------- " + Thread.currentThread().getName() + "加一成功------------ ,值为:" + number);
             notify();
         }
         catch (InterruptedException e) {
             throw new RuntimeException(e);
         }
     }
     public  void decr(){
         try {
             while (number==0){
                 this.wait();
             }
             number--;
             System.out.println("------- " + Thread.currentThread().getName() + "减一成功------------ ,值为:" + number);
             notify();
         }
         catch (InterruptedException e) {
             throw new RuntimeException(e);
         }
     }
 }
复制代码

两个方法上没有对象锁。

3.1.2 先 notify 后 wait

  * 3 将notify放在wait方法前先执行,t1先notify了,3秒钟后t2线程再执行wait方法
  *   3.1 程序一直无法结束
  *   3.2 结论
  *   先wait后notify、notifyall方法,等待中的线程才会被唤醒,否则无法唤醒
  */
 public class LockSupportDemo
 {
 ​
     public static void main(String[] args)//main方法,主线程一切程序入口
     {
         Object objectLock = new Object(); //同一把锁,类似资源类
 ​
         new Thread(() -> {
             synchronized (objectLock) {
                 objectLock.notify();
             }
             System.out.println(Thread.currentThread().getName()+"\t"+"通知了");
         },"t1").start();
 ​
         //t1先notify了,3秒钟后t2线程再执行wait方法
         try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { e.printStackTrace(); }
 ​
         new Thread(() -> {
             synchronized (objectLock) {
                 try {
                     objectLock.wait();
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
             }
             System.out.println(Thread.currentThread().getName()+"\t"+"被唤醒了");
         },"t2").start();
     }
 }
复制代码

先wait后notify、notifyall方法,等待中的线程才会被唤醒,否则无法唤醒.

3.2 Condition 接口的 await 和 signal 方法实现线程的等待和唤醒

3.2.1 正常使用

 /**
  * 使用 Condition 来实现线程通信
  * 此时,完成案例:三个线程依次打印 A、B、C
  * @Author xiaolei
  * @Date 2022/12/9 13:30
  **/
 public class ConditionCommunication {
     public static void main(String[] args) {
         Resource2 resource2 = new Resource2();
         new Thread(()->{
             for (int i = 0; i < 10; i++) {
                 resource2.soutA();
             }
         },"线程A").start();
         new Thread(()->{
             for (int i = 0; i < 10; i++) {
                 resource2.soutB();
             }
         },"线程B").start();
         new Thread(()->{
             for (int i = 0; i < 10; i++) {
                 resource2.soutC();
             }
         },"线程C").start();
     }
 }
 class Resource2{
     private int number =0;
     ReentrantLock lock = new ReentrantLock();
     Condition condition = lock.newCondition();
     Condition condition2 = lock.newCondition();
     Condition condition3 = lock.newCondition();
 ​
     public void soutA(){
         lock.lock();
         try {
             while (number!=0){
                 condition.await();
             }
             System.out.println(Thread.currentThread().getName()+"输出A");
             System.out.println("A");
             number=1;
             // signal 是精准唤醒线程,而 notify 是随机唤醒
             condition2.signal();
         }catch (Exception e){
 ​
         }finally {
             lock.unlock();
         }
     }
     public void soutB(){
         lock.lock();
         try {
             while (number!=1){
                 condition2.await();
             }
             System.out.println(Thread.currentThread().getName()+"输出B");
             System.out.println("B");
             number=2;
             condition3.signal();
         }catch (Exception e){
 ​
         }finally {
             lock.unlock();
         }
     }
     public void soutC(){
         lock.lock();
         try {
             while (number!=2){
                 condition3.await();
             }
             System.out.println(Thread.currentThread().getName()+"输出C");
             System.out.println("C");
             number=0;
             condition.signal();
         }catch (Exception e){
 ​
         }finally {
             lock.unlock();
         }
     }
 }
复制代码

3.2.2 异常两种情况

1)去掉 Lock 和 unLock 后,会触发同样的都触发了IllegalMonitorStateException异常 。使用这个必须配合 Lock 和 unLock 使用

2)如果先唤醒后阻塞,那么程序会一直阻塞,注意执行顺序。

3.3 LockSupport 的 park 和 unPark

按顺序打印 A、B、C 三个数。

 public class LockSupportCommDemoV2 {
 ​
     public static void main(String[] args) {
         Thread t1 = new Thread(new Resource4("A"));
         Thread t2 = new Thread(new Resource4("B"));
         Thread t3 = new Thread(new Resource4("C"));
         t1.start();
         t2.start();
         t3.start();
         // 发车
         LockSupport.unpark(t1);
         for (int i = 0; i < 12; i++) {
             if(i%3==0){
                 LockSupport.unpark(t1);
             }else if(i%3 ==1){
                 LockSupport.unpark(t2);
             }else{
                 LockSupport.unpark(t3);
             }
             try {
                 Thread.sleep(1000);
             } catch (InterruptedException e) {
             }
         }
     }
 }
 class Resource4 implements Runnable{
     private String number;
    public Resource4(String number){
        this.number = number;
    }
     @Override
     public void run() {
         while (true){
             System.out.println(this.number);
             LockSupport.park(this);
         }
     }
 }
复制代码

LockSupport 是 JDK 中用来实现线程阻塞和唤醒的工具,使用它可用在任何场合使线程阻塞,可用指定任何线程进行唤醒,并且不用担心阻塞和唤醒操作的顺序,连续多次唤醒和一次唤醒的效果是一样的。

与其他的区别:

  • 可用先唤醒后等待
  • 简洁优雅使用,无需结束 对象锁 或 lock\unLock 实现。
  • 三种实现机制不同,unpark 无法对 wait 进行唤醒,notify 也无法对 park 进行唤醒。

四、源码分析

LockSupport 和 CAS 是 Java 并发包中很多并发工具控制机制的基础,它们的底层都是依赖 UnSafe 实现的(后续章节再介绍)。

这个类与使用它的每个线程关联一个许可证(类似 Semaphore 信号量),如果许可证可用,就停车呼叫将立即返回,并在过程中使用掉该许可证;否则它就阻塞。调用 unPark 将使许可证可用(许可证最多一个)。

该方法被设计用于创建更高级别的同步实用程序工具,而这些方法本身对大多数并发控制应用程序并不有用,park 方法仅适用于以下结构。

  while (!canProceed()) { 
   ... 
   LockSupport.park(this); 
 }
复制代码

即我们最好按照这种格式来写我们的 LockSupport 代码,通过 while 条件 判断共享资源是否满足条件,然后再通过 是否进行 park 消费,因为一旦执行这句话,它就会立马返回结果。

4.1 park 方法

Disables the current thread for thread scheduling purposes unless the permit is available.(除非我能获取许可证,否则我就禁用该线程)

If the permit is available then it is consumed and the call returns immediately; (如果给我的许可证可用,我就消费它,然后立即返回)

otherwise the current thread becomes disabled for thread scheduling purposes and lies dormant until one of three things happens:(否则当前线程就会一直阻塞直到下面三种情况)

  • Some other thread invokes unpark with the current thread as the target; or(其他线程以当前线程为目标调用 unPark)
  • Some other thread interrupts the current thread; or (其他线程中断当前线程)
  • The call spuriously (that is, for no reason) returns.(或错误返回,即没有原因的)
  // 阻塞当前线程,并把当前线程的 parkBlocker 字段设置为 blocker
  public static void park(Object blocker) {
         // 获取当前线程
         Thread t = Thread.currentThread();
          //将当前线程的parkBlocker字段设置为blocker
         setBlocker(t, blocker);
         //阻塞当前线程,第一个参数表示isAbsolute,是否为绝对时间,第二个参数就是代表时间
         UNSAFE.park(false, 0L);
          //重新可运行后再此设置Blocker
         setBlocker(t, null);
     }
复制代码

调用 park 方法时,首先获取当前线程,然后设置当前线程的 parkBlocker 字段,即调用 setBlocker 函数,然后才调用 UNSAFE 的park 方法,后又调用 setBlocker ,为什么调用两次呢?

 //设置线程t的parkBlocker字段的值为arg
 private static void setBlocker(Thread t, Object arg) {
     // Even though volatile, hotspot doesn't need a write barrier here.
     //尽管hotspot易变,但在这里并不需要写屏障。
     UNSAFE.putObject(t, parkBlockerOffset, arg);
 }
复制代码

因为 在执行到 UNSAFE.park 的时候,这个线程已经阻塞到这里了,我告知其他线程,这个线程已经阻塞了,然后再进行 park,当其他线程调用 unpark 时,就执行第二个 setBlocker,然后它就知道,我解放了,然后将这个线程的 blocker 设置为 null。

4.2 unpark 方法

将指定的线程许可置为可用,也就相当于唤醒了阻塞对象。

     public static void unpark(Thread thread) {
         if (thread != null)
             UNSAFE.unpark(thread);
     }
复制代码

五、小结

LockSupport 本身的方法非常少,作为一个工具类使用,提供接口间接操作 Unsafe 类,源码也不多,对比之前学的线程通信方式更为优雅,另外,多了解一种线程通信方法,然后对后续的深入学习来说,这个类还是非常有必要学习了解的。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

视频监控在油气长输管道巡护管理的应用解决方案

一、方案背景 由于油气产地与消费中心位置的不一致性&#xff0c;常常需要采用长距离的油气管道运输。从偏僻的矿区到繁华的街市&#xff0c;管道架设的环境十分复杂&#xff0c;一旦发生危险&#xff0c;后果将不堪设想。因此&#xff0c;为确保管道安全运行&#xff0c;消除…

redis高可用之主从复制,哨兵,集群

目录 前言 一、主从复制 1、主从复制的作用 2、主从复制流程 3、部署Redis 主从复制步骤 3.1 环境准备 3.2 首先要搭建redis&#xff0c;并关闭防火墙 3.3 修改Redis 配置文件(Master节点操作) 3.4 修改Redis 配置文件(Slave节点操作) 3.5 验证主从效果 二、哨兵 1…

App逆向案例 X嘟牛 - Frida监听 WT-JS工具还原(一)

App逆向案例 X嘟牛 - Frida监听 & WT-JS工具还原&#xff08;一&#xff09; 提示&#xff1a;文章仅供参考&#xff0c;禁止用于非法途径&#xff1b; 文章目录App逆向案例 X嘟牛 - Frida监听 & WT-JS工具还原&#xff08;一&#xff09;前言一、资源推荐二、App抓包分…

基于android平台的语音机器人服务娱乐系统

分 类 号&#xff1a;TP311 学校代码&#xff1a;11460 学 号&#xff1a;10130920 本科生毕业论文 基于android平台的语音机器人服务娱乐系统 Robot Entertainment Service System Based on Android Platform 所在系&#xff08;院&#xff09;&#xff1a; 学 生&…

如何通过快解析建设“智慧工地”,实现远程管理维护

A企业是我国某大型房地产企业&#xff0c;早在几年前就实现了全国化布局&#xff0c;目前除了住宅开发与销售、商用地产开发与销售及持有运营业务外&#xff0c;还涉猎房地产金融、物业服务与社区经营等领域。作为中国房地产的知名企业&#xff0c;从2020年起&#xff0c;A企业…

CentOS虚拟机搭建Hadoop集群

注&#xff1a;本文是对 https://www.bilibili.com/video/BV1CU4y1N7Sh 的实践。 环境 CentOS 7.7JDK 8Hadoop 3.3.0 准备 VMWare的网络设置&#xff1a;略。 准备好3台虚拟机&#xff0c;其IP地址分别为 192.168.88.151 、 192.168.88.152 、 192.168.88.153 &#xff0c…

RK3399 Android 8.1 开机动画制作全流程详解

文章目录一、开机动画包二、开机动画图片三、desc.txt编写规范四、开机动画临时生效五、开机动画内置系统一、开机动画包 N个文件夹和一个desc.txt。文件夹中是开机动画的图片资源&#xff0c;按照文件名顺序播放。desc.txt是开机动画的播放规范。压缩包必须是zip&#xff0c;…

力扣(LeetCode)1781. 所有子字符串美丽值之和(C++)

模拟 & 哈希集合 使用哈希集合&#xff0c;开字符集&#xff0c;下标对应小写字母顺序&#xff0c;值对应字符出现次数。 所有子字符串&#xff0c;根据示例看出&#xff0c;是连续子字符串。那么枚举起点&#xff0c;再枚举子字符串长度&#xff0c;就是所有连续子字符串…

web网页大作业:基于html设计与实现的茶文化网站12页(带psd)

&#x1f389;精彩专栏推荐 &#x1f4ad;文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业&#xff1a; 【&#x1f4da;毕设项目精品实战案例 (10…

如何使用PyMySQL模块进行增删改查?

在正式动手之前&#xff0c;我们需要先安装 PyMySQL 模块。 &#xff08;1&#xff09;使用 pip 安装, 清华镜像&#xff1a; pip install -i https://pypi.tuna.tsinghua.edu.cn/simple pymysql &#xff08;2&#xff09;使用 conda 安装 conda install pymysql Step2: …

毕业/课程设计——基于STM32的智能灯光控制系统(智能家居、手机APP控制、语音控制)

文章首先介绍本系统所包含的功能&#xff0c;主要包含六方面功能&#xff0c;之后逐步分享开发过程&#xff0c;其流程如下&#xff1a;点亮灯带&#xff08;三极管&#xff09;→调节灯光亮度&#xff08;PWM&#xff09;→为系统添加远程控制功能→为系统添加语音识别功能→添…

【脚本项目源码】Python实现鲁迅名言查询系统

前言 本文给大家分享的是如何通过利用Python实现鲁迅名言查询系统&#xff0c;废话不多直接开整~ 开发工具 Python版本&#xff1a; 3.6 相关模块&#xff1a; PyQt5模块 fuzzywuzzy模块 环境搭建 安装Python并添加到环境变量&#xff0c;pip安装需要的相关模块即可。 …

R语言使用最优聚类簇数k-medoids聚类进行客户细分

k-medoids聚类简介 k-medoids是另一种聚类算法&#xff0c;可用于在数据集中查找分组。k-medoids聚类与k-means聚类非常相似&#xff0c;除了一些区别。k-medoids聚类算法的优化功能与k-means略有不同。最近我们被客户要求撰写关于聚类的研究报告&#xff0c;包括一些图形和统…

【JavaWeb开发-Servlet】将项目部署在云服务器

目录 1、环境 &#xff08;1&#xff09;连接服务器 &#xff08;2&#xff09;安装JDK1.8​编辑 ①到官网下载环境 ②将压缩包上传至linux中 ③解压jdk压缩包 ④修改文件名 ⑤配置环境变量 &#xff08;3&#xff09;安装MySQL5.0 ①先下载MySQL的yum库 ②选择5.7发行版本…

基于C#+SQL Server(WinForm)学生选课及成绩查询管理系统【100010027】

学生选课及成绩查询管理系统的设计与开发 1、项目背景 学生选课及成绩查询系统是一个学校不可缺少的部分&#xff0c;传统的人工管理档案的方式存在着很多的缺点&#xff0c;如&#xff1a;效率低、保密性差等&#xff0c;所以开发一套综合教务系统管理软件很有必要&#xff…

Shell基础

获取命令结果$? 如果上一个命令是成功的则结果为0 否则为127 结果是 www.qfdu.com is down! EOF使用 sh 其实是bash的一个链接&#xff0c;本质上还是执行bash 然后脚本第一句表示使用什么执行器&#xff0c;如果写的是python但是你执行的时候不指定python会报错&#x…

Linux安装Mysql(图文解说详细版,安装包tar包版)

上次教大家用yum安装mysql https://blog.csdn.net/csdnerM/article/details/121095527&#xff0c; 结果还要小伙伴不知道tar包怎么安装&#xff0c;现在出个tar包安装的教程出来供大家参考 文章目录&#x1f3e2;1.官网下载tar包&#xff08;安装包&#xff09;&#x1f3e3;…

多个著名 Go 开源项目被放弃,做大开源不能用爱发电,更不能只靠自己!

大家好&#xff0c;我是煎鱼。相信关注我的许多同学都有接触 Go 语言的开发&#xff0c;甚至在企业中多有实践。那么你在日常开发中&#xff0c;势必会接触到 gorilla[1] 组织下的各个 Go 开源项目。如下图&#xff1a;gorilla/mux[2]&#xff1a;Star&#xff1a;17.9k。a pow…

VMWare复制CentOS虚拟机后无法联网

1. 现象描述 原来使用一个 CentOS 虚拟机&#xff0c;这里叫 CentOS_1。网络配置如下&#xff1a;VMWare软件设置 NAT 模式&#xff0c;虚拟机配置 DHCP 自动获取 IP &#xff0c;开机启动网络。可以正常联网 现在要重新配置一个虚拟机&#xff0c;环境同 CentOS_1。因此我直…

视频恢复软件哪个好用?推荐这几款恢复率高的软件

如果你意外删除了电脑保存的视频文件&#xff0c;无论是单击回收站“还原”还是通过电脑系统备份“还原”&#xff0c;它都无法恢复。如何解决这个问题&#xff1f;此时&#xff0c;你需要寻求文件删除恢复软件的帮助&#xff0c;推荐下面这几款恢复率高的视频恢复软件。如何操…