Java 并发编程面试题——Java 线程间通信方式

目录

  • 1.✨Java 线程间有哪些通信方式?
    • 1.1.volatile 和 synchronized 关键字
    • 1.2.等待/通知机制
      • 1.2.1.概述
      • 1.2.2.经典范式
    • 1.3.管道输入/输出流
    • 1.4.信号量
  • 2.Thread.join() 有什么作用?它的使用场景是什么?
  • 3.Java 中需要主线程等待子线程执行完毕后再执行,有哪些方法可以实现?

参考:《Java 并发编程的艺术》

1.✨Java 线程间有哪些通信方式?

1.1.volatile 和 synchronized 关键字

(1)Java 支持多个线程同时访问一个对象或者对象的成员变量,由于每个线程可以拥有这个变量的拷贝(虽然对象以及成员变量分配的内存是在共享内存中的,但是每个执行的线程还是可以拥有一份拷贝,这样做的目的是加速程序的执行,这是现代多核处理器的一个显著特性),所以程序在执行过程中,一个线程看到的变量并不一定是最新的。

(2)关键字 volatile 可以用来修饰字段(成员变量),就是告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性

举个例子,定义一个表示程序是否运行的成员变量 boolean on = true,那么另一个线程可能对它执行关闭动作(on = false),这里涉及多个线程对变量的访问,因此需要将其定义成为 volatile boolean on=true,这样其他线程对它进行改变时,可以让所有线程感知到变化,因为所有对 on 变量的访问和修改都需要以共享内存为准。但是,过多地使用 volatile 是不必要的,因为它会降低程序执行的效率

(3)关键字 synchronized 可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性

有关 synchronized 与 volatile 的具体知识可以参考 Java 并发编程面试题——synchronized 与 volatile 这篇文章。

1.2.等待/通知机制

1.2.1.概述

(1)等待/通知的相关方法是任意 Java 对象都具备的,因为这些方法被定义在所有对象的超类 java.lang.Object 上:

在这里插入图片描述

(2)等待/通知机制是指:一个线程 A 调用了对象 O 的 wait() 方法进入等待状态,而另一个线程 B 调用了对象 O 的 notify() 或者 notifyAll() 方法,线程 A 收到通知后从对象 O 的 wait() 方法返回,进而执行后续操作。上述两个线程通过对象 O 来完成交互,而对象上的 wait()notify/notifyAll() 的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。

(3)下面的例子中创建了两个线程——WaitThreadNotifyThread,前者检查 flag 值是否为 false,如果符合要求,进行后续操作,否则在 lock 上等待,后者在睡眠了一段时间后对 lock 进行通知。

class WaitNotify {static boolean flag = true;static Object lock = new Object();public static void main(String[] args) throws Exception {Thread waitThread = new Thread(new Wait(), "WaitThread");waitThread.start();TimeUnit.SECONDS.sleep(1);Thread notifyThread = new Thread(new Notify(), "NotifyThread");notifyThread.start();}static class Wait implements Runnable {public void run() {//加锁,拥有 lock 的 Monitorsynchronized (lock) {//当条件不满足时,继续 wait,同时释放了 lock 的锁while (flag) {try {System.out.println(Thread.currentThread() + " flag is true. wait @ " +new SimpleDateFormat("HH:mm:ss").format(new Date()));lock.wait();} catch (InterruptedException e) {}}// 条件满足时,完成工作System.out.println(Thread.currentThread() + " flag is false. running @ " +new SimpleDateFormat("HH:mm:ss").format(new Date()));}}}static class Notify implements Runnable {public void run() {//加锁,拥有 lock 的 Monitorsynchronized (lock) {/*获取 lock 的锁,然后进行通知,通知时不会释放 lock 的锁,直到当前线程释放了 lock 后,WaitThread 才能从 wait 方法中返回*/System.out.println(Thread.currentThread() + " hold lock. notify @ " +new SimpleDateFormat("HH:mm:ss").format(new Date()));lock.notifyAll();flag = false;SleepUtils.second(5);}// 再次加锁synchronized (lock) {System.out.println(Thread.currentThread() + " hold lock again. sleep @ " +new SimpleDateFormat("HH:mm:ss").format(new Date()));SleepUtils.second(5);}}}
}class SleepUtils {public static final void second(long seconds) {try {TimeUnit.SECONDS.sleep(seconds);} catch (InterruptedException e) {}}
}

输出如下(输出内容可能不同,主要区别在时间上):

Thread[WaitThread,5,main] flag is true. wait @ 16:03:28
Thread[NotifyThread,5,main] hold lock. notify @ 16:03:29
Thread[NotifyThread,5,main] hold lock again. sleep @ 16:03:34
Thread[WaitThread,5,main] flag is false. running @ 16:03:39

上述第 3 行和第 4 行输出的顺序可能会互换,而上述例子主要说明了调用 wait()、notify() 以及 notifyAll() 时需要注意的细节,如下:

  • 使用 wait()、notify() 和 notifyAll() 时需要先对调用对象加锁
  • 调用 wait() 方法后,线程状态由 RUNNING 变为 WAITING,并将当前线程放置到对象的等待队列。
  • notify() 或 notifyAll() 方法调用后,等待线程依旧不会从 wait() 返回,需要调用 notify() 或 notifAll() 的线程释放锁之后,等待线程才有机会从 wait() 返回。
  • notify() 方法将等待队列中的一个等待线程从等待队列中移到同步队列中,而 notifyAll() 方法则是将等待队列中所有的线程全部移到同步队列,被移动的线程状态由 WAITING 变为 BLOCKED
  • 从 wait() 方法返回的前提是获得了调用对象的锁

(4)从上述细节中可以看到,等待/通知机制依托于同步机制,其目的就是确保等待线程从 wait() 方法返回时能够感知到通知线程对变量做出的修改。下图描述了上述示例的过程:

在这里插入图片描述

在上图中,WaitThread 首先获取了对象的锁,然后调用对象的 wait() 方法,从而放弃了锁并进入了对象的等待队列 WaitQueue 中,进入等待状态。由于 WaitThread 释放了对象的锁,NotifyThread 随后获取了对象的锁,并调用对象的 notify() 方法,将 WaitThread 从 WaitQueue 移到 SynchronizedQueue 中,此时 WaitThread 的状态变为阻塞状态。NotifyThread 释放了锁之后,WaitThread 再次获取到锁并从 wait() 方法返回继续执行。

有关抽象队列同步器的相关知识可以参考 Java 并发编程面试题——Lock 与 AbstractQueuedSynchronizer (AQS) 这篇文章。

1.2.2.经典范式

(1)从上节中的示例中可以提炼出等待/通知的经典范式,该范式分为两部分,分别针对等待方(消费者)和通知方(生产者)。

(2)等待方遵循如下原则。

  • 获取对象的锁。
  • 如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件。
  • 条件满足则执行对应的逻辑。

对应的伪代码如下:

synchronized (对象) {
while (条件不满足) {对象.wait();
}对应的处理逻辑
}

(3)通知方遵循如下原则:

  • 获得对象的锁。
  • 改变条件。
  • 通知所有等待在对象上的线程。

对应的伪代码如下:

synchronized(对象) {改变条件对象.notifyAll();
}

1.3.管道输入/输出流

(1)管道输入/输出流和普通的文件输入/输出流或者网络输入/输出流不同之处在于:它主要用于线程之间的数据传输,而传输的媒介为内存。管道输入/输出流主要包括了如下 4 种具体实现:PipedOutputStreamPipedInputStreamPipedReaderPipedWriter,前两种面向字节,而后两种面向字符

(2)下面的例子中创建了 printThread,它用来接受 main 线程的输入,任何 main 线程的输入均通过 PipedWriter 写入,而 printThread 在另一端通过 PipedReader 将内容读出并打印。

import java.io.IOException;
import java.io.PipedReader;
import java.io.PipedWriter;
import java.util.*;
import java.util.concurrent.*;public class Piped {public static void main(String[] args) throws Exception {PipedWriter out = new PipedWriter();PipedReader in = new PipedReader();//将输出流和输入流进行连接,否则在使用时会抛出 IOExceptionout.connect(in);Thread printThread = new Thread(new Print(in), "PrintThread");printThread.start();int receive = 0;try {while ((receive = System.in.read()) != -1) {out.write(receive);}} finally {out.close();}}static class Print implements Runnable {private PipedReader in;public Print(PipedReader in) {this.in = in;}public void run() {int receive = 0;try {while ((receive = in.read()) != -1) {System.out.print((char) receive);}} catch (IOException ex) {}}}
}

运行该示例,输入一组字符串,可以看到被 printThread 进行了原样输出。

Repeat my words.
Repeat my words.

对于 Piped 类型的流,必须先要进行绑定,也就是调用 connect() 方法,如果没有将输入/输出流绑定起来,对于该流的访问将会抛出异常。

1.4.信号量

(1)Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。Semaphore 可以用于做流量控制,特别是公用资源有限的应用场景。

(2)举个例子,6 辆汽车抢占 3 个车位:

// 6 辆汽车,抢占 3 个停车位
class SemaphoreDemo {//创建许可数量private static final int N = 3;public static void main(String[] args) {//创建 Semaphore 对象,并设置许可数量Semaphore semaphore = new Semaphore(N);//模拟 6 辆汽车for (int i = 1; i <= 6; i++) {new Thread(() -> {try {//抢占semaphore.acquire();System.out.println(Thread.currentThread().getName() + " 抢到了车位(即抢占资源)!");//设置随机停车时间TimeUnit.SECONDS.sleep(new Random().nextInt(5));System.out.println(Thread.currentThread().getName() + " ------离开了车位(即释放资源)!");} catch (InterruptedException e) {e.printStackTrace();} finally {//释放semaphore.release();}}, String.valueOf(i)).start();}}
}

某次运行的输出结果如下:

1 抢到了车位(即抢占资源)!
4 抢到了车位(即抢占资源)!
2 抢到了车位(即抢占资源)!
1 ------离开了车位(即释放资源)!
2 ------离开了车位(即释放资源)!
6 抢到了车位(即抢占资源)!
5 抢到了车位(即抢占资源)!
4 ------离开了车位(即释放资源)!
3 抢到了车位(即抢占资源)!
5 ------离开了车位(即释放资源)!
6 ------离开了车位(即释放资源)!
3 ------离开了车位(即释放资源)!

Semaphore 是 JUC 包下的一个常用并发工具类,有关其具体知识可以参考 Java 并发编程面试题——JUC 并发工具类 这篇文章。

2.Thread.join() 有什么作用?它的使用场景是什么?

(1)Thread.join() 是一个线程同步的方法,它的作用是让当前线程等待被调用 join() 方法的线程执行完毕。线程 Thread 除了提供 join() 方法之外,还提供了 join(long millis)join(long millis, int nanos) 两个具备超时特性的方法。这两个超时方法表示,如果线程thread在给定的超时时间里没有终止,那么将会从该超时方法中返回。例子如下:

class JoinDemo {static class Task implements Runnable {@Overridepublic void run() {try {System.out.println("我是线程 t1,我先 sleep 2 秒");Thread.sleep(2000);System.out.println("我是线程 t1,我 sleep 完了 2 秒");} catch (InterruptedException e) {e.printStackTrace();}}}public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(new Task(), "t1");t1.start();t1.join();System.out.println("如果不加 t1.join() 方法,我会先被打印出来");}
}

输出结果如下:

我是线程 t1,我先 sleep 2 秒
我是线程 t1,我 sleep 完了 2 秒
如果不加 t1.join() 方法,我会先被打印出来

(2)下面的代码是 JDK 中 Thread.join() 方法的源码(进行了部分调整):

// 加锁当前线程对象
public final synchronized void join() throws InterruptedException {// 条件不满足,继续等待while (isAlive()) {wait(0);}// 条件符合,方法返回
}

当线程终止时,会调用线程自身的 notifyAll() 方法,会通知所有等待在该线程对象上的线程。可以看到 join() 方法的逻辑结构与上面描述的等待/通知经典范式一致,即加锁循环处理逻辑这 3 个步骤。

(3)Thread.join() 方法的使用场景包括:

  • 等待其他线程的结果:在多线程中,有时候需要等待其他线程执行完毕后再进行后续操作,这时可以使用 join() 方法等待其他线程的完成。
  • 线程顺序控制:通过 join() 方法可以控制线程的执行顺序,即确保某个线程在其他线程执行完毕后再开始执行。

需要注意的是,如果线程执行过程中发生异常或被中断,join() 方法也会立即返回

3.Java 中需要主线程等待子线程执行完毕后再执行,有哪些方法可以实现?

在 Java 中,有几种方法可以实现主线程等待子线程执行完毕后再执行的需求:

  • 使用 Thread.join() 方法:在主线程中,可以通过调用子线程对象的 join() 方法等待子线程执行完毕。join() 方法会阻塞主线程,直到子线程执行完毕才会继续执行主线程的后续代码。示例代码如下:
Thread myThread = new Thread(() -> {// 子线程的执行逻辑
});// 启动子线程
myThread.start();try {// 等待子线程执行完毕myThread.join();
} catch (InterruptedException e) {e.printStackTrace();
}// 子线程执行完毕后,继续执行主线程的后续代码
  • 使用CountDownLatch类:CountDownLatch 可以用于实现线程之间的计数等待。在主线程中,可以创建一个 CountDownLatch 对象,并指定等待的子线程数。在子线程中,在执行完毕后调用 countDown() 方法进行计数减一。主线程可以通过调用 await() 方法等待计数变为 0。示例代码如下:
CountDownLatch latch = new CountDownLatch(1);Thread myThread = new Thread(() -> {// 子线程的执行逻辑// 子线程执行完毕后,计数减一latch.countDown();
});// 启动子线程
myThread.start();try {// 等待子线程执行完毕latch.await();
} catch (InterruptedException e) {e.printStackTrace();
}// 子线程执行完毕后,继续执行主线程的后续代码
  • 使用 ExecutorService 线程池:可以通过 ExecutorService 来管理线程池,并使用 submit() 方法提交子线程的任务。然后,可以通过调用shutdown() 方法关闭线程池,并通过调用 awaitTermination() 方法等待所有任务执行完毕。示例代码如下:
ExecutorService executorService = Executors.newFixedThreadPool(1);executorService.submit(() -> {// 子线程的执行逻辑
});executorService.shutdown();try {// 等待所有任务执行完毕executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
} catch (InterruptedException e) {e.printStackTrace();
}// 子线程执行完毕后,继续执行主线程的后续代码

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

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

相关文章

java:封装统一的响应体code、data、msg、paging

背景 我们在写接口的时候一般不会直接返回给前端数据&#xff0c;而是会有响应体&#xff0c;比如 code、data、msg&#xff0c;这样就有一个统一的结构方便前端处理&#xff0c;那么今天就来封装一个统一的响应体 封装基本响应体 1、在 config 包里新建 ApiResponse.java …

5+铜死亡+预后模型+分型生信思路,热点搭配免疫相关思路

今天给同学们分享一篇生信文章“The pathogenesis of DLD-mediated cuproptosis induced spinal cord injury and its regulation on immune microenvironment”&#xff0c;这篇文章发表在Front Cell Neurosci期刊上&#xff0c;影响因子为5.3。 结果解读&#xff1a; 基因芯…

LeetCode - 110. 平衡二叉树(C语言,二叉树,配图,简单)

根据题意&#xff0c;我们只需要比较当前节点的左右子树高度差是否小于1&#xff0c;利用分治法&#xff0c;只需要满足&#xff1a; 1. 根节点的左右子树的高度差小于1。 2. 根节点左右子树的满足高度差小于1&#xff0c;在往下走&#xff0c;判断左子树根节点的左右子树是否满…

质量检查管理制度

质量检查管理制度 建立健全质量检查管理制度&#xff0c;明确过程检查、最终检查、质量评定、检查记录和检查报告等要求

40.坑王驾到第六期:这是什么奇葩错误?

一、问题再现 在HbuilderX中引用了一个checkALL模块&#xff0c;正常编译然后启动到微信开发者工具&#xff0c;工具报错&#xff0c;找不到XXXXXX/index2.js。为什么编译后在微信工具中会自动加一个2呢&#xff1f;&#xff1f;&#xff1f;&#xff1f; 二、解决问题 经过清…

【Node.js】基础梳理 6 - MongoDB

写在最前&#xff1a;跟着视频学习只是为了在新手期快速入门。想要学习全面、进阶的知识&#xff0c;需要格外注重实战和官方技术文档&#xff0c;文档建议作为手册使用 系列文章 【Node.js】笔记整理 1 - 基础知识【Node.js】笔记整理 2 - 常用模块【Node.js】笔记整理 3 - n…

【LeetCode】每日一题 2023_12_5 到达首都的最少油耗(树,搜索)

文章目录 刷题前唠嗑题目&#xff1a;到达首都的最少油耗题目描述代码与解题思路 刷题前唠嗑 LeetCode&#xff1f;启动&#xff01;&#xff01;&#xff01; 题目&#xff1a;到达首都的最少油耗 题目链接&#xff1a;2477. 到达首都的最少油耗 题目描述 代码与解题思路 …

Python代码编译并生成Docker镜像

Python代码编译并生成Docker镜像 前言 实际python项目交付时往往有针对关键代码进行保护的需求&#xff0c;本文介绍了一种简单可行的方案&#xff1a;1. 在Linux系统上先将 .py 文件编译为 .so 文件&#xff0c;2. 将整个项目打包成Docker镜像&#xff08;解决 .so 文件的环…

【性能测试】业务/吞吐量与存量数据设计关系+压测常见解决方案

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

C语言错误处理之 “信号处理方式<signal.h>及signal函数等内置函数”

目录 前言 signal.h头文件 信号宏 signal函数 实例&#xff1a;在Linux环境下验证signal函数 实例&#xff1a;在Linux中演示保存signal函数的返回值 预定义的信号处理函数&#xff08;简单了解&#xff09; SIG_DFL函数 SIG_IGN函数 raise函数 实例&#xff1a;测试…

leetcode 255.用队列实现栈

255.用队列实现栈 不出意外大概率这几天都会更新 leetcode&#xff0c;如果没有做新的题&#xff0c;大概就会把 leetcode 之前写过的题整理&#xff08;单链表的题目居多一点&#xff09;出来写成博客 今天讲的题蛮容易出错的&#xff08;注意传参啊&#xff0c;最好把队列的…

渗透测试——五、网站漏洞——SQL注入

一、走进DVWA测试网站 1、网站渗透测试步骤 (1)收集信息 1、获取域名的 Whois 信息&#xff0c;获取注册者邮箱、姓名、电话等。2、查询服务器旁站及子域名站点&#xff0c;因为主站一般比较难&#xff0c;所以先看看旁站有没有通用性的CMS或者其他漏洞。3、查看服务器操作系…

ROS话题通信基本操作(C++)

目录 一、发布 1、实现步骤 2、代码实例 二、接收 1、实现步骤 2、代码实例 三、配置运行 1、修改CMakeLists.txt 2、运行结果 一、发布 1、实现步骤 1.包含头文件 2.初始化 ROS 节点:命名(唯一) 3.实例化 ROS 句柄 4.实例化 发布者 对象 5.组织被发布的数据&#…

Pytest做性能测试?

Pytest其实也是可以做性能测试或者基准测试的。是非常方便的。 可以考虑使用Pytest-benchmark类库进行。 安装pytest-benchmark 首先&#xff0c;确保已经安装了pytest和pytest-benchmark插件。可以使用以下命令安装插件&#xff1a; pip install pytest pytest-benchmark …

JSP控制项目启动后默认去访问指定的WebServlet 而不是index.jsp

我的 index.jsp 代码是这样 现在每次启动 访问的都是index.jsp 这也是它的默认配置 我这里写了一个 WebServlet 代码是这样 简单可以理解为 我们定义了WebServlet 访问路径为1cginServlet 其中在request作用域中 定义了一个userName值为 欢迎来到jsp世界 然后 跳转向 page.j…

Android : AndroidStudio开发工具优化

1.开启 gradle 单独的守护进程 Windows: 进入目录 C:\Users\Administrator\.gradle 创建文件&#xff1a; gradle.properties # Project-wide Gradle settings. # IDE (e.g. Android Studio) users: # Settings specified in this file will override any Gradle s…

前端时间的失败总结复盘

分享失败经验&#xff0c;前段时间的总结复盘&#xff1a; 与伙伴合作面对异常决策要及时提出质疑&#xff0c;怼&#xff0c;别太客气&#xff0c;客气起来&#xff0c;小心翼翼在意他人情绪那么这个项目就会让人难受&#xff0c;不要因为因为伙伴身上有标签/光环/权威就觉得…

怎么把视频转换为gif?

怎么把视频转换为gif&#xff1f;视频和 GIF 动图在许多方面都有所不同&#xff0c;主要的区别包括它们的数据格式、播放方式以及用途。视频&#xff1a;通常以常见的视频格式如MP4、AVI、MOV等存储&#xff0c;它们包含了连续的图像帧以及音频轨道&#xff08;如果有的话&…

ISP算法简述-BLC

Black Level Calibration, 黑电平矫正 现象 1)在纯黑条件下拍张图&#xff0c;你会发现像素值不为0 2)或者你发现图像整体偏色 这些问题可能是黑电平导致的。 原因 存在黑电平的原因有2个&#xff1a; 1)sensor的电路本身存在暗电流。暗电流主要产生在光电信号转换过程中&#…

葡萄酒的储存条件会影响葡萄酒的陈酿吗?

自20世纪末以来&#xff0c;葡萄酒储存有关的行业一直在增长&#xff0c;一些葡萄酒鉴赏家可能会选择将葡萄酒存放在家里的专用房间或壁橱里。 自云仓酒庄品牌雷盛红酒分享总所周知&#xff0c;瓶装葡萄酒的储存条件是会影响葡萄酒的陈酿&#xff0c;振动和热波动会加速葡萄酒的…