Java线程间通信方式

news2024/10/5 15:28:57

前文了解了线程的创建方式和状态切换,在实际开发时,一个进程中往往有很多个线程,大多数线程之间往往不是绝对独立的,比如说我们需要将A和B 两个线程的执行结果收集在一起然后显示在界面上,又或者比较典型的消费者-生产者模式,在这些场景下,线程间通信成了我们必须使用的手段,那么线程之间怎么通信呢?

线程间通信方式,从实现本质来讲,主要可以分为两大类共享内存和消息传递。

相信大家还记得,在内存模型一节,我们提到多线程并发情况下的三大特性,原子性,有序性,可见性,其所对应的解决方案就可以用来实现线程间通信,这些解决方案的本质就是共享内存。

对于消息传递而言,最经典的实现就是我们的Handler机制,在子线程使用主线程的Handler对象将一些信息发送到主线程以便进行处理。

下面我们来看一些线程间通信的典型实现

Object.wait/Object.notify

对于Object对象而言,其提供了等待/通知机制以便实现线程间通信,由于Object是Java中所有类的父类,也就意味着Java中所有对象都支持通知/等待机制,与该机制关联的主要有五个方法:

方法名称描述备注
wait()线程执行中调用对象的wait方法可以使得当前线程进入WAITING状态,只有等待其他线程的通知或被中断才会返回,需要注意的是,调用wait方法后,会释放对象的锁/
wait(long timeout)与wait含义一致,不同的是通过timeout指定了超时时间,如果时间到了还没收到通知就超时返回/
wait(long timeout, int nanos)超时管控更加精确,第二个参数单位为毫微秒/
notify通知一个在对象上等待的线程使其从wait对象返回/
notifyAll通知所有等待在该对象上的线程/

以Object.wait/Object.notify实现一个典型的消息者生产者模型,消费者对变量做-1操作,生产者对变量做+1操作,代码如下:

// 盘子
public class Number {
    // 盘子当前容量,是否有内容
    private int mCount = 0;
  
    //对盘子容量进行+1操作
    public void inc() {
        if (mCount != 0) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        mCount++;
        System.out.println(Thread.currentThread().getName()+",mCount+1,mCount="+mCount);
        this.notifyAll();
    }

    //对盘子容量进行-1操作
    public void dec() {
        if (mCount == 0) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        mCount--;
        System.out.println(Thread.currentThread().getName()+",mCount-1,mCount="+mCount);
        this.notifyAll();
    }
}
    public static void main(String[] args) {
        Number number = new Number();
        // 生产者线程
        Thread incThread = new Thread(new Runnable() {
            @Override
            public void run() {
                number.inc();
            }
        });
        incThread.setName("Inc Thread");
        incThread.start();

        // 消费者线程
        Thread decThread = new Thread(new Runnable() {
            @Override
            public void run() {
                number.dec();
            }
        });
        decThread.setName("Dec Thread");
        decThread.start();
    }

如上述代码备注,其中Inc Thread为生产者线程,当盘子内容为0时,每次向盘子Number中放一个内容,消费者线程Dec Thread当盘子有内容时,消耗内容,让盘子内容变为0.运行输出如下:

1-4-3-1

糟糕,正确运行一个循环后,抛出了IllegalMonitorStateException,为什么会这样呢?这个异常是什么意思?

遇事不决看源码,IllegalMonitorStateException的类说明如下:

Thrown to indicate that a thread has attempted to wait on an object’s monitor or to notify other threads waiting on an object’s monitor without owning the specified monitor.

翻译过来的意思就是当线程在没有持有特定的锁的情况下试图等待对象锁或者通知其他线程等待对象锁会抛出此异常,有点拗口,先放置,即然我们调用了wait/notifyAll这两个方法,不妨看下这两个方法的说明,看是否有新的提示,wait方法说明如下:

/**
 * Causes the current thread to wait until another thread invokes the
 * {@link java.lang.Object#notify()} method or the
 * {@link java.lang.Object#notifyAll()} method for this object.
 * In other words, this method behaves exactly as if it simply
 * performs the call {@code wait(0)}.
 * <p>
 * The current thread must own this object's monitor. The thread
 * releases ownership of this monitor and waits until another thread
 * notifies threads waiting on this object's monitor to wake up
 * either through a call to the {@code notify} method or the
 * {@code notifyAll} method. The thread then waits until it can
 * re-obtain ownership of the monitor and resumes execution.
 * <p>
 * As in the one argument version, interrupts and spurious wakeups are
 * possible, and this method should always be used in a loop:
 * <pre>
 *     synchronized (obj) {
 *         while (&lt;condition does not hold&gt;)
 *             obj.wait();
 *         ... // Perform action appropriate to condition
 *     }
 * </pre>
 * This method should only be called by a thread that is the owner
 * of this object's monitor. See the {@code notify} method for a
 * description of the ways in which a thread can become the owner of
 * a monitor.
 *
 * @throws  IllegalMonitorStateException  if the current thread is not
 *               the owner of the object's monitor.
 * @throws  InterruptedException if any thread interrupted the
 *             current thread before or while the current thread
 *             was waiting for a notification.  The <i>interrupted
 *             status</i> of the current thread is cleared when
 *             this exception is thrown.
 * @see        java.lang.Object#notify()
 * @see        java.lang.Object#notifyAll()
 */
public final void wait() throws InterruptedException {
    wait(0);
}

在上述说明中反复提到 The current thread must own this object’s monitor. This method should only be called by a thread that is the owner of this object’s monitor.也就是说在调用Object.wait方法前,当前线程必须持有该对象的锁,获取锁的方法很简单,wait方法说明中也有,通过synchronized关键词,那么正确的调用代码如下所示:

public class Number {
    private int mCount = 0;
    public void inc() {
        synchronized (this) {
            if (mCount != 0) {
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            mCount++;
            System.out.println(Thread.currentThread().getName()+",mCount+1,mCount="+mCount);
            this.notifyAll();
        }
    }

    public void dec() {
        synchronized (this) {
            if (mCount == 0) {
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            mCount--;
            System.out.println(Thread.currentThread().getName()+",mCount-1,mCount="+mCount);
            this.notifyAll();
        }
    }
}

重新运行代码,输出如下:

1-4-3-2

这里可以看出,只运行了一个循环,那么怎么让它一直运行呢?将if修改称while即可,以生产10次为例,如需一直生产消息,使用while(true)即可,代码及输出如下:

public class Number {
    private int mCount = 0;
    private int mIncTimes = 0;
    private int mDecTimes = 0;
    public void inc() {
        synchronized (this) {
            while (mIncTimes < 10) {
                if (mCount != 0) {
                    try {
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                mCount++;
                mIncTimes ++;
                System.out.println(Thread.currentThread().getName()+",mCount+1,mCount="+mCount+",mIncTimes:"+mIncTimes);
                this.notifyAll();
            }
        }
    }

    public void dec() {
        synchronized (this) {
            while (mDecTimes < 10) {
                if (mCount == 0) {
                    try {
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                mCount--;
                mDecTimes++;
                System.out.println(Thread.currentThread().getName()+",mCount-1,mCount="+mCount+",mDecTimes:"+mDecTimes);
                this.notifyAll();
            }
        }
    }
}

1-4-3-3

综上,使用Object.wait/Object.notify/Object.notifyAll时,切记其必须先使用关键词获取同个Object的对象锁,否则就会抛出IllegalMonitorStateException异常

Semaphore

Semaphore翻译为信号量,一个信号量维护一组许可,在调用acquire方法时阻塞,直到获取许可,在调用release的时候释放占用,从而唤醒阻塞的某个线程。信号量操作类似于停车场车辆管理,初始时停车场有5个车位,当停车场内部5个车位全占满时,此时可用资源为0,即信号量可用许可数量为0,其他车辆想停车就只能在停车场外排队阻塞(相当于调用acquire),当一辆车辆从停车场驶出时(相当于调用release方法),此时信号量许可数量为1,唤醒一个等待停车的车辆进入停车辆,自身可用许可数量再次为0,依此往复。

对于只有一个许可的信号量而言,其可用许可数量为0或1,故被称为二进制信号量,对于有多个正整数可用许可数据的信号量而言,其被称为通用信号量。需要注意在执行acquire时信号量本身并不会持有同步锁,因为这样会影响被释放的许可进入可用许可池中。

二进制信号量,不同于其他锁机制,要求释放锁的线程和获取锁的线程是同一个,也就意味着我们可以在其他线程释放二进制信号量以完成死锁恢复。

下面我们以二进制信号量实现消费者生产者模式,代码如下(生产消费4次即停止):

public class Counter {
    private int mCount = 0;
    public void incCount() {
        mCount ++;
    }

    public void decCount() {
        mCount--;
    }

    public int getCount() {
        return mCount;
    }
}

// Main主类代码
private static int mIncTimes = 0;
public static void main(String[] args) {
    Counter counter = new Counter();
    Semaphore semaphore = new Semaphore(1);
    Thread incThread = new Thread(new Runnable() {
        @Override
        public void run() {
            while (mIncTimes < 4) {
                try {
                    semaphore.acquire();
                    if (counter.getCount() == 0) {
                        counter.incCount();
                        mIncTimes ++;
                        System.out.println("Inc Thread ++,current count is:" + counter.getCount());
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release();
                }
            }
        }
    });
    incThread.setName("Inc Thread");
    incThread.start();

    Thread decThread = new Thread(new Runnable() {
        @Override
        public void run() {
            while (mIncTimes < 4) {
                try {
                    semaphore.acquire();
                    if (counter.getCount() != 0) {
                        counter.decCount();
                        System.out.println("Dec Thread --,current count is:" + counter.getCount());
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release();
                }
            }
        }
    });
    decThread.setName("Dec Thread");
    decThread.start();
}

运行结果如下:

1-4-3-6

内存一致性影响,要求一个线程中的release操作和另一个线程中的acquire操作必须存在happen-before关系

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

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

相关文章

Linux内核进程管理与调度:策略优化与实践分析

Linux内核进程管理与调度 一、前言二、进程管理和多进程调度2.1 进程标识符和控制块2.2 进程状态和转换2.3 进程间通信 三、单处理器下的Linux进程调度3.1 Linux进程调度器3.2 时间片轮转调度算法3.3 最短剩余时间优先调度算法3.4 其他调度算法的不足 四、多处理器下的Linux进程…

【数据结构 ---基于C语言预备知识】

数据结构 ---基于C语言预备知识 一、对数据结构这门课的基础认识1.1 数据结构的定义&#xff1a;1.2 衡量算法的标准&#xff1a;主要的是1&#xff0c;21.3 数据结构的特点和地位&#xff1a; 二、预备知识&#xff1a;2.1 内存概念&#xff1a;2.2 预备知识指针&#xff08;复…

每日学术速递4.23

CV - 计算机视觉 | ML - 机器学习 | RL - 强化学习 | NLP 自然语言处理 Subjects: cs.CV 1.LiDAR-NeRF: Novel LiDAR View Synthesis via Neural Radiance Fields 标题&#xff1a;LiDAR-NeRF&#xff1a;通过神经辐射场的新型 LiDAR 视图合成 作者&#xff1a;Tang Tao, …

pytest 学习三(前置后置操作)

pytest测试框架_pytest框架-CSDN博客 一、常用的操作 一、setup/teardown 每个用例之前、之后执行 二、setup_class/teardown_class 在每个类之前、之后执行一次 二、pytest.fixture 设置前置后置操作范围 pytest.fixture(scope"",params,autouse,ids,name) 其中 sc…

TVM- End-to-End Optimization Stack for Deep Learning

TVM- End-to-End Optimization Stack for Deep Learning 引言 TensorFlow、MXNet、Caffe 和 PyTorch 等可扩展框架推动了深度学习当前的普及和实用性。然而&#xff0c;这些框架针对范围较窄的服务器级 GPU 进行了优化&#xff0c;将工作负载部署到其他平台&#xff08;例如手…

SpringBoot中@EnableAsync和@Async注解的使用

目录 1.EnableAsync 注解1.1 配置类使用示例1.2 复制请求上下文 2.用法1&#xff1a;Async 注解2.1 测试Controller2.2 测试Service2.3 测试ServiceImpl2.4.测试 4.用法2&#xff1a;直接使用 taskExecutor 做异步4.1 重新实现&#xff1a;测试ServiceImpl4.2 测试 5.Async异步…

保护模式段描述符

目前为止&#xff0c;内存还是分段模式&#xff0c;所以想要保护内存&#xff0c;就需要保存段。由于CPU的扩展导致了32位的段基地址和段内偏移&#xff0c;所以16位的段寄存器就无法放下这些信息。现在就需要把这些信息放到内存中&#xff0c;这些信息被封装成特定的段描述符。…

Vue进阶-Vue cli项目搭建、项目基本操作、axios的使用、路由、Vuex

Vue进阶 Vue cli 一、Vue cli 概述 CLI 全称是 Commond Line Interface&#xff0c;翻译为命令行界面&#xff0c;俗称脚手架。VueCLI是一个官方发布vue.js项目脚手架。用VueCLI 可快速搭建Vue开发环境以及对应webpack配置。 二、环境搭建 1、下载 node.js 下载地址&…

4 redis高可用

所谓的高可用&#xff0c;也叫HA&#xff08;High Availability&#xff09;&#xff0c;是分布式系统架构设计中必须考虑的因素之一&#xff0c;它通常是指&#xff0c;通过设计减少系统不能提供服务的时间。如果在实际生产中&#xff0c;如果redis只部署一个节点&#xff0c;…

Compiler- 自增运算

我们来看一下C语言中的前自增&#xff08;i&#xff09;和后自增(i) 这个经典案例。大家在学习C的时候肯定学过前自增是先自增&#xff0c;然后将结果用于计算&#xff1b;后自增是先参与计算&#xff0c;再增加。 好&#xff0c;看一下这段代码的结果&#xff1a; #include …

PE文件反编译为python脚本流程

1、查壳 DetectltEasy、PeiD查壳 2、脱壳 常见打包工具PyInstaller&#xff0c;脱壳方法 &#xff08;1&#xff09;用pyinstxtractor.py脱壳&#xff0c;用”python pyinstxtractor.py 1.exe“命令&#xff0c;生成“.exe文件名_extracted” &#xff08;2&#xff09;用…

Python+Qt人脸识别门禁管理系统

程序示例精选 PythonQt人脸识别门禁管理系统 如需安装运行环境或远程调试&#xff0c;见文章底部个人QQ名片&#xff0c;由专业技术人员远程协助&#xff01; 前言 这篇博客针对<<PythonQt人脸识别门禁管理系统>>编写代码&#xff0c;代码整洁&#xff0c;规则&am…

1 ROS2介绍与安装

1 ROS2介绍与安装 1.1 Ubuntu配置与ROS2安装1.1.1 Ubuntu22.04安装1.1.2 下载安装ROS21.1.3 配置ROS2环境并测试 1.2 使用VSCode搭建ROS2开发环境1.2.1 安装并配置VSCode1.2.2 创建ROS2工程的方法1.2.3 使用VSCode创建ROS2的C/C项目1.2.4 使用VSCode创建ROS2的Python项目 1.3 R…

Samba配置回收站功能

部门确实需要给Samba配置回收站&#xff0c;查阅了下回收站的资料&#xff0c;配置也挺简单的。 配置说明&#xff1a; 在Samba配置回收站功能中各参数作用如下。 (1) vfs object recycle&#xff1a;载入Samba用于回收站功能的模块recycle.so。 (2) recycle:repository /Pr…

企业如何保护外发文件的数据安全?

随着数字化转变&#xff0c;企业的业务文件大多通过电子形式在内外部流转。这增加了外发文件数据泄露或被篡改的风险&#xff0c;如何保护外发文件安全已成为企业不容忽视的课题。 企业外发文件&#xff0c;特别是电子文件&#xff0c;存在一定的数据安全风险&#xff1a; 文件…

第十二章 外观模式

文章目录 前言一、外观模式基本介绍完整代码DVD类爆米花类投影仪类屏幕类立体声类灯光类家庭影院类进行聚合Client测试类 二、 外观模式在MyBatis框架应用的源码分析三、外观模式的注意事项和细节 前言 一、外观模式基本介绍 完整代码 DVD类 package tanchishell.SJMS.faca…

Layui 2.8.0 正式发布,朴实归来

Layui 是一套开源的 Web UI 组件库&#xff0c;采用自身轻量级模块化规范&#xff0c;遵循原生态的 HTML/CSS/JavaScript 开发模式&#xff0c;极易上手&#xff0c;拿来即用。其风格简约轻盈&#xff0c;而内在雅致丰盈&#xff0c;甚至包括文档在内的每一处细节都经过精心雕琢…

On the Efficacy of Knowledge Distillation 解析

paper&#xff1a;On the Efficacy of Knowledge Distillation 本文的题目是《论知识蒸馏的有效性》&#xff0c;主要是对教师模型并不是越大越好这一现象进行研究&#xff0c;并提出了缓解方法&#xff1a;early stop。 Bigger models are not better teachers 知识蒸馏背…

S32k3系列开发学习(FlexCAN)

前言 由于之前没有接触过CAN总线模块&#xff0c;对这一块的知识仍比较陌生&#xff0c;于是乎想简单梳理一下CAN总线的工作流程&#xff0c;加深理解。 一、CAN是什么&#xff1f; 参考&#xff1a;https://zhuanlan.zhihu.com/p/346696648 二、CAN框架 各模块功能如下&am…

NLP基础:标注器Label Studio的入门使用

目录 一、环境准备 二、操作 文章来源&#xff1a; 简介&#xff1a; Label Studio是一个开源的数据标注工具&#xff0c;它可以用于各种机器学习和深度学习项目。它的主要目的是帮助数据科学家和机器学习工程师快速、高效地标注数据&#xff0c;以构建和训练准确的机器学…