【多线程系列-04】深入理解java中线程间的通信机制

news2024/11/17 9:44:57

多线程系列整体栏目


内容链接地址
【一】深入理解进程、线程和CPU之间的关系https://blog.csdn.net/zhenghuishengq/article/details/131714191
【二】java创建线程的方式到底有几种?(详解)https://blog.csdn.net/zhenghuishengq/article/details/127968166
【三】深入理解java中线程的生命周期,任务调度https://blog.csdn.net/zhenghuishengq/article/details/131755387
【四】深入理解java中线程间的通信机制https://blog.csdn.net/zhenghuishengq/article/details/132072145

深入理解java中线程间的通信机制

  • 一,线程间的通信、协调和协作
    • 1,通道
    • 2,join
    • 3,synchronized
    • 4,volatile
    • 5,等待通知机制

一,线程间的通信、协调和协作

前面几篇谈到了单线程的各个属性,接下来在谈线程与线程之间是如何进行通信和协调的

1,通道

在进程中,可以通过管道的方式进行通信,在java线程中,也是可以通过管道的方式进行通信的。如一些文件的上传,可以直接在内存中通过管道的方式进行文件的上传,而不需要先将文件落盘到本地,再将文件上传到ftp服务器上,通过减少写入磁盘这一步骤,从而提高文件的上传效率,减少硬件和资源等的成本。

java中实现管道输入和输出的方式主要有四种,分别是PipedOutputStream、PipedInputStream、PipedReader 和 PipedWriter 。前面两种主要是针对二进制的字节流,后面两种主要是针对文本的字符流。

//构建输入流
PipedReader pipedReader = new PipedReader();
//构建输出流
PipedWriter pipedWriter = new PipedWriter();
try {
    //建立连接
    pipedReader.connect(pipedWriter);
} catch (IOException e) {
    e.printStackTrace();
}

并且在高并发中,这些管道流的操作都是属于线程安全的。

2,join

在日常开发中,比如说存在三个线程,分别是t1,t2,t3这三个线程,需求是想让t2在t1执行完后再执行,t3想再t2执行完后再执行。由于java中采用的是抢占式的线程调度方式,即不能手动的去操控线程的执行状况,因此在后面就出现了这个join 的方式。如下面的代码

public static void main(String[] args){
     Thread t1 = new Thread(new Runnable() {
         @Override
         public void run() {
             try {
                 //休眠2s
                 Thread.sleep(2000);
                 System.out.println(Thread.currentThread().getName());
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         }
     },"t1");
     Thread t2 = new Thread(new Runnable() {
         @Override
         public void run() {
             try {
                 //加入join
                 t1.join();
                 System.out.println(Thread.currentThread().getName());
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         }
     },"t2");
     t1.start();
     t2.start();
 }

可以通过执行结果发现线程t2需要在t1执行完成之后,才会继续执行。

3,synchronized

在jvm中,每个线程都会对应一个虚拟机栈中的栈帧,里面存在操作数栈,局部变量表等,每个线程中的内部参数是线程安全的,但是如果存在一个全局变量,会通过多个线程去操作,那么就会出现不可预料的后果,就是所谓的线程安全的问题。

因此在java中引入了一种阻塞状态,就是通过这个synchronized关键字,来解决多个线程操作一个全局变量可能出现的不可预料的问题。这种关键字可以加载方法上,也可以加在对象的同步块上面。这种方式相当于把并发的访问变成了串行的访问。

//对象锁加在方法上
public synchronized void countAdd(){count++;}
//对象锁加在同步块上
synchronized(this){count++}
//类锁加在静态方法上
public synchronized static void countAdd(){count++;}
//类锁加在其他对象上
synchronized(object){count++}

因此在日常开发中,对号使用对象锁。因为类锁锁的不是同一个对象,因此可能出现锁失效的情况。

synchronized这种方式主要是确保在多线程的同一时刻情况下,有且仅有一个线程可以处于方法或者同步块中,并且同时保证线程对变量访问的可见性和排他性,使得多个线程在操作同一个变量的时候让结果正确。因此在java线程的阻塞状态,就是通过这个关键字来实现的。后续的文章会详细的讲述这个关键字的底层原理

4,volatile

上面谈到了这个synchronized关键字,但是在java中该关键字属于重量级操作,有可能要对操作系统进行调度,从用户态到内核态之间来回的切换等,因此就出现了一个轻量级操作的关键字 volatile

该关键字可以保证不同线程对某个变量操作的即时可见性,即某一个线程一旦将某个变量的值给改了,那么其他线程是立马可以感应到的。

如下面这段代码,同时开启一个main主线程和一个子线程,在主线程中将变量的值改了,如果变量不加这个volatile关键字,那么子线程将会一直卡住进入死循环,如果变量加了这个volatile关键字,子线程就可以立马的感受到其他线程修改了这个变量的值,从而获取到修改后的值,跳出循环

/**
 * @author zhenghuisheng
 * @date : 2023/7/28
 */
public class VolatileTest {
    private static volatile boolean flag = false;
    private static volatile int count = 0;

    public static class InnerTest extends Thread{
        @Override
        public void run() {
            while (!flag){

            }
            System.out.println("当前线程不阻塞了,count值为:" + count);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        //开启线程
        new InnerTest().start();
        //休眠一段时间,让子线程空转一会
        Thread.sleep(2000);
        //主线程将数据替换
        flag = true;
        count = 100;
        //替换完成之后,子线程立马感知看到这个修改的数据
        Thread.sleep(2000);
        System.out.println("main主线程执行完毕");
    }
}

volatile不是一把真正的锁,所以不能保证数据的原子性,即如果在高并发情况下在变量上使用该关键字,会出现安全问题。该关键字主要适用的场景是:一个线程读、多个线程写

5,等待通知机制

在上篇线程的生命周期中讲过,线程有等待和超时等待这两种状态,线程的等待停止机制就是通过这两种状态来实现的,如典型的生产者消费者模式。

在这里插入图片描述

等待和唤醒有着其标准的规范,无论是任何,都需要遵循以下的范式:不管是等待方还是通知方,都需要在锁的范围内使用,如果不在锁的范围内使用,则会抛出异常

等待方的范式如下

加锁(对象){
    while(条件不满足){
        对象.wait方法
    }
    进入后面的业务逻辑
}

通知方的范式如下

加锁(对象){
    业务逻辑,改变消费者不满足的条件
    对象.notify方法(通知方法)
}

上面的等待方和通知方都需要拿同一把锁,但是当等待方调用wait的时候,等待方所持有的锁将会释放,那么通知方自然而然的就可以拿到这把锁,去做业务逻辑,从而改变这个条件。

接下来针对于这个范式写一个案例,就是简单的生产者与消费者之间的案例,生产者每2s生产100个产品,满了500个产品的时候,通知消费者消费,代码逻辑实现就是如果此时产品的数量小于500,那么消费者线程则处于等待状态,当生产者满了500个,就通过notify或者notifyAll唤醒处于等待的线程。

首先定义一个Product的产品类,其代码如下

/**
 * @Author: zhenghuisheng
 * @Date: 2023/8/1 23:19
 */
public class Product {
    //唤醒的数量
    public static final int finalCount = 500;
    //成员变量
    public int count;
    public Product(int count){
        this.count = count;
    }
    //给生产者改变数量
    public void addCount(){
        count = count + 100;
    }
    //消费者线程
    public synchronized void waitCount() throws InterruptedException {
        //不满足条件
        while (this.count < finalCount){
            System.out.println("线程即将等待");
            //等待
            wait();
            //业务逻辑
            System.out.println("消费者消费了" + this.count + "公里数");
        }
    }
}

随后定义一个测试类TestP,里面定义一个线程消费类consum,然后使用主线程作为生产者的线程,生产者将产品生产好了,就去通知消费线程去消费

/**
 * @Author: zhenghuisheng
 * @Date: 2023/8/1 23:56
 */
public class TestP {
    private static  Product product = new Product(0);
    //定义一个消费线程类
    private static class consum extends Thread{
        @Override
        public void run() {
            try {
                product.waitCount();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    //主线程作为生产线程
    public static void main(String[] args) throws InterruptedException {
        //开启线程
        new consum().start();
        Thread.sleep(500);
        //累加
        synchronized (product){
            for (int i = 0; i < ; i++) {
                product.addCount();
            }
            product.notifyAll();
        }
    }

}

需要注意的是,在使用wait和notify时,都需要在代码的同步块或者成员的同步方法里面,并且在一般场景中,可以使用的notifyAll的就不用notify,因为notify只唤醒一个线程,不利于在多线程中的操作。notify不支持唤醒某个指定的线程,可以通过显示锁来实现。

notify或者notifyAll尽量写在同步块的最后面,因为在调用该方法之后,会直接释放锁。

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

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

相关文章

如何通过 5 步激活策略扩大用户群

假设&#xff0c;你现在是一个“深藏功与名”的增长黑客。前期你表现非常好&#xff0c;做了一些拉新实验&#xff0c;每天都有上千用户进入到产品。团队成员和家人朋友都非常开心你们的产品增长终于有了起色。 然而&#xff0c;如果你不重视拉新&#xff08;acquisition&…

TI的IWR6843跑3D People Tracking(3D人体检测追踪实验)demo的上手教程

1.硬件准备 1.IWR6843板子 2.两个USB转串口模块&#xff08;因为我的是自己做的板子&#xff0c;板子上没有集成USB转串口芯片&#xff09; 2.软件准备 1.最新版本的CCS&#xff0c;注意后缀没有THEIA https://www.ti.com/tool/CCSTUDIO?DCMPdsp_ccs_v4&HQSccs 2.最新…

速通pytorch库

速通pytorch库&#xff08;长文&#xff09; 前言 ​ 本篇文章主要为那些对于pytorch库不熟悉、还没有上手的朋友们准备&#xff0c;梳理pytorch库的主要内容&#xff0c;帮助大家入门深度学习最重要的库之一。 目录结构 文章目录 速通pytorch库&#xff08;长文&#xff09;1.…

13 springboot项目——准备数据和dao类

13.1 静态资源下载 https://download.csdn.net/download/no996yes885/88151513 13.2 静态资源位置 css样式文件放在static的css目录下&#xff1b;static的img下放图片&#xff1b;template目录下放其余的html文件。 13.3 创建两个实体类 导入依赖&#xff1a;lombok <!…

35岁后的测试工程师出路?测试工程师能干多久?

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 你有没有一刹那意…

服务器硬件、部署LNMP动态网站、部署wordpress、配置web与数据库服务分离、配置额外的web服务器

day01 day01项目实战目标单机安装基于LNMP结构的WordPress网站基本环境准备配置nginx配置数据库服务部署wordpressweb与数据库服务分离准备数据库服务器迁移数据库配置额外的web服务器 项目实战目标 主机名IP地址client01192.168.88.10/24web1192.168.88.11/24web2192.168.88…

产业大数据应用:精准剖析区域产业,摸家底 明方向 促发展

随着信息技术的飞速发展&#xff0c;大数据和新一代信息技术的崛起&#xff0c;这些技术的应用正在逐渐渗透到各个领域&#xff0c;在区域产业发展上&#xff0c;他们不仅为区域产业诊断分析带来了高效的工具&#xff0c;更为区域制定产业发展战略和政策提供了有效的数据支撑。…

活动预告 | 中国数据库联盟(ACDU)中国行第二站定档杭州,邀您探讨数据库技术与实践!

数据库技术一直是信息时代中不可或缺的核心组成部分&#xff0c;随着信息量的爆炸式增长和数据的多样化&#xff0c;其重要性愈发凸显。作为中国数据库联盟&#xff08;ACDU&#xff09;的品牌活动之一&#xff0c;【ACDU 中国行】在线下汇集数据库领域的行业知名人士&#xff…

Keil出现Flash Timeout.Reset the Target and try it again.我有一种解决方法

2.解决方法 网上查找了找原因&#xff0c;是因为之前代码设置了读保护功能。 读保护即大家通常说的“加密”&#xff0c;是作用于整个Flash存储区域。一旦设置了Flash的读保护&#xff0c;内置的Flash存储区只能通过程序的正常执行才能读出&#xff0c;而不能通过下述任何一种…

echarts绘制关系图

效果图&#xff1a; 代码&#xff1a; <template><div id"serveGraph" style"height: 100%; width: 100%; z-index: 88;"></div> </template> <script> import { defineComponent,reactive,toRefs,onMounted,watch } from …

网络工程师 快速入门

需要掌握 以下技术 1.网络 基础 知识 TCP/IP 、OSI 7层协议、IP地址、ARP地址解析协议、ICMP&#xff08;英特网控制报文协议&#xff0c;ping&#xff09;等 入门面试常问问题。 2.路由 路由匹配 三原则、静态路由、OSPF路由协议。 2.交换 如何放数据&#xff1f; VLAN TRU…

【Spring】bean的生命周期

1.具体的生命周期过程 bean对象创建&#xff08;调用无参构造器&#xff09; 给bean对象设置属性 bean对象初始化之前操作&#xff08;由bean的后置处理器负责&#xff09; bean对象初始化&#xff08;需在配置bean时指定初始化方法&#xff09; bean对象初始化之后操作&am…

C# Blazor 学习笔记(0.1):如何开始Blazor和vs基本设置

文章目录 前言资源推荐环境如何开始Blazor个人推荐设置注释快捷键热重载设置 前言 Blazor简单来说就是微软提供的.NET 前端框架。使用 WebAssembly的“云浏览器”&#xff0c;集成了Vue,React,Angular等知名前端框架的特点。 资源推荐 微软官方文档 Blazor入门基础视频合集 …

Arcgis地图实战一:单个图层中设施的隐藏及显示

文章目录 1.效果图预览2.弹框的实现3.显示及隐藏的实现 1.效果图预览 2.弹框的实现 let alert this.alertCtrl.create();alert.setTitle(请选择设施);for (let item of this.ctralllayers) {alert.addInput({type: checkbox,label: item.name,value: item.id,checked: item.vi…

音量压低处理流程

开始 通过申请临时DUCK焦点可以压低其他在播放的音源&#xff0c;如源码中的注释&#xff0c;不会暂停其他在播放的音源&#xff0c;而是降低输出&#xff0c;在车载情景下&#xff0c;一般在地图导航或者语音播报的情景下会申请这个焦点。 // AudioManager.java /*** Used t…

Netty自定义消息协议的实现逻辑处理粘包拆包、心跳机制

Netty 自定义消息协议的实现逻辑自定义编码器 心跳机制实现客户端发送心跳包 自定义消息协议的实现逻辑 消息协议&#xff1a;这一次消息需要包含两个部分&#xff0c;即消息长度和消息内容本身。 自定义消息编码器︰消息编码器将客户端发送的消息转换成遵守消息协议的消息&…

Andorid解析XML格式数据遇到的坑

以下是《第一行代码 第三版》解析XML格式数据部分遇到的坑 一、首先是安装Apache遇到的坑 具体参考文章Apache服务器下载安装及使用&#xff08;更新&#xff09;_apache下载_★邱↓邱★的博客-CSDN博客&#xff08;可以不看文中的安装部分了&#xff09; 启动服务那块儿建议…

Java:Map的getOrDefault()方法结果仍为null

1、问题 今天在工作中遇到一个问题&#xff0c;在一个通用的数据处理方法中&#xff0c;方法会从一个Map类型参数通过key里获取对象value&#xff0c;但方法的调用者并不都会传递value实例&#xff0c;若没有获取到value则需要初始化一个&#xff0c;处理方式是调用了Map的getO…

操作系统的运行机制、中断和异常、系统调用

&#x1f40c;个人主页&#xff1a; &#x1f40c; 叶落闲庭 &#x1f4a8;我的专栏&#xff1a;&#x1f4a8; c语言 数据结构 javaweb 石可破也&#xff0c;而不可夺坚&#xff1b;丹可磨也&#xff0c;而不可夺赤。 操作系统 一、操作系统的运行机制1.1内核程序1.2应用程序1…

vue3项目基于vue-router跳转到登录页面

创建项目 #创建项目 #选择vue3 选择npm vue create devops-front#安装vue-router 路由 npm install -g cnpm --registryhttps://registry.npmmirror.com cnpm install vue-router4 #启动项目 vue run serve app.vue 定义<router-view/> 路由入口 <template>&l…