并发编程宝典:剖析Java线程间通信机制 | 多线程篇(四)
环境说明:Windows 10 + IntelliJ IDEA 2021.3.2 + Jdk 1.8
前言
- 同步方法:
- 使用 synchronized 关键字修饰方法实现同步。
- 同步代码块:
- 使用 synchronized 关键字修饰代码块实现同步。
- 使用 Locks 和 Conditions:
- 使用 ReentrantLock、ReentrantReadWriteLock 实现更灵活的同步。
然而,线程安全只是并发编程的一部分。在多线程环境中,线程之间往往需要相互协作,共享数据,以及在满足特定条件时相互通知。这就是线程间通信发挥作用的地方。有效的线程间通信不仅能够提高程序的效率,还能使程序设计更加灵活和强大,这也是我讲论本期内容的核心。
在本期内容中,我们将深入学习Java中实现线程间通信的几种关键技术:wait()、notify() 和 notifyAll() 方法,以及 BlockingQueue。这些知识点将帮助我们开发更加复杂和高效的并发应用程序。
摘要
回顾完后,接下来,我们就来学习本期的内容啦,你们做好准备了没?本文我将详细介绍Java中线程间通信的机制,包括如下:
- 使用
Object
类的wait()
、notify()
和notifyAll()
方法进行线程间的基本通信。 - 通过
BlockingQueue
实现生产者-消费者模式。
文章内容将涵盖这些机制的简介、源码解析、案例分析、应用场景、优缺点分析以及测试案例演示等,一定会把线程通信相关知识点讲解透彻,一定不会让你白阅读此期哒。
正文
一、线程间通信?
谈到通信?还是线程之间,可能很多小伙伴,还一头雾水,线程间通信?匪夷所思,这是个啥概念,但稍微了解前几期的同学,这个概念也不难理解,顾名思义:线程间通信是指多线程程序中不同线程之间进行数据交换和协调的一种机制
,就好比我们人与人需要沟通交流互通消息一样。在并发环境下,由于多个线程可能会访问共享数据,因此需要一种方式来确保数据的一致性和线程的协调运行,这就需要线程间通信了。
1.为什么线程间need通信?
这个就让我来解惑一下,在多线程程序中,不同的线程可能需要依赖其他线程的执行结果,或者需要在满足特定条件时才能继续执行。有效的线程间通信机制可以确保这些依赖关系得到妥善处理,从而避免死锁、竞态条件等问题,所以说在你学习多线程篇,这个知识点,你也是必须要掌握的。
2.线程有哪些通信方式
那么,程序间到底是如何进行线程通信呢?又有哪些渠道或者方式进行通信呢?通信的原理是啥...哈哈,一上来就夺命三问。其实就对于Java编程语言而言,它自身就提供了多种线程间通信的方式,在通信上它使用起来非常便利,完全不需要多深入及学习,我把这些方式总结如下:
- 等待/通知机制:使用
wait()
、notify()
和notifyAll()
方法,线程可以等待某些条件成立或被其他线程通知。 - 锁和条件对象:
Lock
接口和Condition
类提供了更灵活的线程同步机制。 - 并发集合:如
BlockingQueue
,提供了线程安全的集合操作,可以在生产者和消费者之间进行通信。 - 原子变量:
java.util.concurrent.atomic
包下的原子类,支持无锁的线程安全操作。 - 线程池和其他并发工具:如
ExecutorService
,可以调度任务的执行,并提供了任务结果的获取方式。
除了如上这些,你们后续也会接触到些别的通信方式,不必大惊小怪,积极学习接受新事物,保持一颗热衷学习的心,事必成!
3.通信原理
那么学习了如上的通信方式,一猜就能想到大家想问,通信的实现原理是何样的?大概我就总结成一句话,你们就能明白了:“这里线程间通信通常是基于操作系统的同步原语,如互斥锁、信号量等,而Java多线程之间的通信原理主要包括锁与同步、等待/通知机制、信号量(基于volatile关键字的实现)以及Thread.join()方法和ThreadLocal类等”。在Java语言中,这些原语被封装在高级的API中,使得线程间通信更加安全和方便。例如:
Java多线程之间的通信主要依赖于线程同步机制和一些高级的并发工具。以下是一些关键原理和机制:
-
共享变量:
- 线程之间可以通过访问共享变量来通信。共享变量可以是类的成员变量或者静态变量。
- 为了确保共享变量的一致性,通常需要使用同步机制来控制对这些变量的访问。
-
同步机制:
- synchronized关键字:可以用于同步方法或代码块。当一个线程进入一个同步方法或代码块时,它会获得一个锁,其他线程必须等待这个锁释放后才能进入该方法或代码块。
- ReentrantLock:提供了更灵活的锁机制,例如尝试非阻塞获取锁、可中断的锁获取等。
-
volatile关键字:
volatile
关键字确保变量的读写操作对所有线程都是可见的。当一个线程修改了一个volatile
变量的值,其他线程能够立即看到这个变化。
-
等待/通知机制:
- 线程可以通过调用
wait()
方法进入等待状态,直到被其他线程通过调用notify()
或notifyAll()
方法唤醒。 - 这些方法必须在同步代码块或同步方法中调用,以确保线程安全。
- 线程可以通过调用
-
...
通过这些机制,多线程程序可以有效地实现线程之间的通信和同步,确保数据的一致性和线程的安全执行。
4.应用场景
我们学习了线程通信,那肯定下一步就会想到,它可以用到哪些地方或者场景上呢?确实,如果光学习不灵活运用,这肯定是不够的。而我想说,对于线程间通信,它在许多并发场景中都有应用,例如:
- 生产者-消费者问题:例如:生产者生成数据,消费者处理数据,两者通过线程间通信协调工作。
- 任务调度:例如:例如:在复杂的任务执行流程中,线程间通信可以确保任务按正确的顺序执行。
- 数据共享:例如:在需要共享数据的系统中,线程间通信可以确保数据的一致性和线程的安全访问。
除了如上几项经典的场景案例,线程通信还可以运用到很多地方,只需要你评估能够派上用场,提高性能或者提升处理效率且在线程安全的前提下,那么这个应用就是成功且适宜的。
5.线程间通信的重要性
说到场景如何使用,这里就有必要提一嘴它的重要性了。良好的线程间通信机制对于构建高效、可靠和可维护的并发应用程序至关重要。它可以减少线程间的冲突,提高程序的性能和响应能力。同时也希望大家都能够在以后的开发过程中,多把自己学习到的理论知识运用到实践中,真正学以致用。
6.小结
线程间通信是并发编程中的一个核心知识点,理解并掌握它对于每一个Java开发者来说都是必要的。在后续的内容中,我们将详细介绍Java中线程间通信的各种机制,并通过实际的示例来展示它们的使用方式。
通过上述概述,我们为读者提供了线程间通信的基本概念、重要性以及它在Java并发编程中的应用场景。接下来,我们将深入探讨具体的实现方式和最佳实践。
线程间通信是多线程程序设计中的一个关键问题。Java提供了多种方式来实现这一点,其中最基本的是每个对象都拥有的wait()
、notify()
和notifyAll()
方法。这些方法允许线程等待条件变量的改变或其他线程的操作。此外,java.util.concurrent
包中的BlockingQueue
接口及其实现类提供了一种高效的方式来实现生产者-消费者模式。
二、通信常用方法解析
如下我们先来探探我们上述所提到的线程间通信的这几种方式的原理,学习前辈开发这些产物的思想。
1.wait()、notify() 和 notifyAll()
这些方法是定义在java.lang.Object
类中的,因此所有的Java对象都继承了它们。
wait()
:使当前线程等待,直到另一个线程调用相同对象的notify()
或notifyAll()
方法。notify()
:唤醒在此对象监视器上等待的单个线程。notifyAll()
:唤醒在此对象监视器上等待的所有线程。
注意:这些方法必须在同步方法或同步块中调用。
如下,我们来扒扒它们的部分源码,学习开发它们的实现原理。如下我就以wait()方法为例。
简单一看,wait()
方法是Object
类的一个公共实例方法,它用于使当前线程等待,直到另一个线程调用了此对象的notify()
、notifyAll()
方法,或者当前线程的interrupt()
方法被调用。这个方法主要用于线程间的协调,以实现条件变量的等待机制。
以下是使用wait()
方法的一些关键点:
-
使当前线程等待:调用
wait()
方法的线程将会释放它所持有的对象锁,并进入该对象的等待集(wait set),直到以下三种情况之一发生:- 另一个线程调用了此对象的
notify()
方法,并且当前线程被选中唤醒。 - 另一个线程调用了此对象的
notifyAll()
方法,唤醒所有等待该对象的线程。 - 当前线程的
interrupt()
方法被调用,导致线程抛出InterruptedException
。
- 另一个线程调用了此对象的
-
超时参数:
wait()
方法有一个重载版本,允许指定一个超时时间:wait(long timeout)
。如果指定了超时时间,线程将在指定的时间内等待,超时后即使没有收到通知也会自动唤醒。 -
响应中断:如果线程在
wait()
期间被中断,wait()
方法将抛出InterruptedException
。这是Java中响应中断的标准做法,允许线程在等待过程中被唤醒并处理中断。 -
必须在同步上下文中调用:
wait()
方法必须在同步方法或同步块中调用,因为线程需要获取对象的锁才能调用wait()
。 -
与
sleep()
的区别:与Thread.sleep()
不同,Thread.sleep()
不会释放对象锁,而wait()
会释放对象锁,允许其他线程进入同步块或同步方法。
在如上我提供的源码代码片段中:
这段代码实际上调用了另一个重载的wait()
方法,并传递了0
作为超时时间。这意味着线程将无限期地等待,直到收到通知或被中断。传递0
作为超时参数通常不是一个好的做法,因为它不会为线程提供任何超时保障,如果需要超时机制,应该传递一个合理的正数作为超时时间。
正确的使用方式可能是:
synchronized (someObject) {
while (!condition) {
someObject.wait();
}
// 条件满足,执行后续操作
}
在这个例子中,someObject
是当前线程持有的锁,condition
是等待的条件。如果条件不满足,线程将调用wait()
并释放锁,进入等待状态。当条件满足时,另一个线程会调用notify()
或notifyAll()
来唤醒等待的线程。
至于notify()
和notifyAll()
,我就作为课后作业,留给同学们去研究了,此处我就一笔带过,不细讲了哈。
2.BlockingQueue
至于BlockingQueue
,它是Java并发包java.util.concurrent
中定义的一个接口,它有几个实现类如ArrayBlockingQueue
、LinkedBlockingQueue
等。它继承自Queue
接口,并提供了一些额外的线程安全操作方法。而且BlockingQueue
非常适合用于实现生产者-消费者模式,因为它能够在队列满或空时阻塞或唤醒线程。
如下列举几个BlockingQueue接口中非常关键的方法,它们提供了基本的生产者-消费者队列操作,并且是线程安全的。如下:
put(E e)
:如果可能,将元素放入队列中,等待空间变得可用。take()
:从队列中取出一个元素,如果队列为空,则等待。
如下是部分BlockingQueue
源码的截图,这个同学们跟随我一同来扒扒看。
a.主要特点
- 线程安全:所有的
BlockingQueue
实现都是线程安全的。这意味着多个线程可以同时访问同一个队列实例,而不需要额外的同步措施。 - 有界或无界:
BlockingQueue
可以是有界的,也可以是无界的。例如,ArrayBlockingQueue
是有界的,而LinkedBlockingQueue
可以设置为无界。 - 阻塞操作:当队列操作不能立即执行时(如队列已满或空),
BlockingQueue
提供了阻塞等待的机制,而不是立即抛出异常。
b.常用实现
- ArrayBlockingQueue:一个由数组支持的有界阻塞队列。
- LinkedBlockingQueue:一个由链表支持的可选有界阻塞队列。
- PriorityBlockingQueue:一个按优先级排序的阻塞队列。
c.核心方法介绍
put(E e)
:向队列中添加一个元素。如果队列已满,则等待(或抛出IllegalStateException
,如果使用offer
方法)直到队列有足够空间。take()
:从队列中取出并返回一个元素。如果队列为空,则等待直到有元素可用。offer(E e, long timeout, TimeUnit unit)
:如果可能,添加一个元素到队列中,等待直到超时时间。poll(long timeout, TimeUnit unit)
:如果可能,取出并返回队列头部的元素,等待直到超时时间。drainTo(Collection<? super E> c)
:将队列中的所有元素转移到给定的集合中,直到队列为空。
d.使用场景
- 生产者-消费者问题:
BlockingQueue
是实现生产者-消费者问题的理想选择,因为它可以自动处理线程间的协调。 - 任务调度:在线程池中,
BlockingQueue
常用于存放待执行的任务。 - 数据缓存:在分布式系统中,
BlockingQueue
可以用于缓存数据,控制数据的流入和流出。
e.方法使用示例代码
package com.secf.service.action.hpy.day4;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
/**
* BlockingQueue使用示例
*
* @Author bug菌
* @Source 公众号:猿圈奇妙屋
* @Date 2024-06-27 16:35
*/
public class BlockingQueueExample {
private final BlockingQueue<Integer> queue = new LinkedBlockingQueue<>();
// 生产者线程
public void produce() throws InterruptedException {
for (int i = 0; i < 10; i++) {
queue.put(i); // 将元素放入队列
System.out.println("Produced: " + i);
}
}
// 消费者线程
public void consume() throws InterruptedException {
for (int i = 0; i < 10; i++) {
int item = queue.take(); // 从队列中取出元素
System.out.println("Consumed: " + item);
}
}
public static void main(String[] args) {
BlockingQueueExample example = new BlockingQueueExample();
BlockingQueue<Integer> queue = example.queue;
Thread producer = new Thread(() -> {
try {
example.produce();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Thread consumer = new Thread(() -> {
try {
example.consume();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
producer.start();
consumer.start();
}
}
在这个示例中,我主要是创建了一个BlockingQueue
的实例,并分别定义了生产者和消费者方法。生产者向队列中添加元素,而消费者从队列中取出元素。我们只需要启动main函数,即可启动生产者和消费者线程,就能演示BlockingQueue
如何在多线程环境中同步线程操作。
本地测试执行结果如下:
f.小结
根据如上对BlockingQueue
的讲解及演示,毋庸置疑它是Java并发编程中一个非常好用且及有用的工具,它简化了线程间的协调和通信。通过BlockingQueue
的使用,开发者可以更容易地实现复杂的并发模式,如生产者-消费者问题,而无需手动管理线程同步。
三、应用案例场景
- 生产者-消费者问题:这是线程间通信最典型的应用场景之一。(上述我就给大家演示了!)
- 任务调度:线程池中的工作线程从任务队列中获取任务执行。
- 数据缓存:使用线程安全的队列作为缓存,控制数据的流入和流出。
四、优缺点对比
根据如上介绍的几种方法,如下我们来简单对比下,这些方法都有何优缺点。
1.优点概括如下:
- wait()/notify():简单易用,适用于需要精细控制线程通信的场景。
- BlockingQueue:提供了一种高效且线程安全的方式来处理队列操作。
2.缺点概括如下:
- wait()/notify():如果使用不当,容易造成死锁或资源竞争。
- BlockingQueue:在高并发场景下,性能可能成为瓶颈。
五、测试案例演示
接下来,我便通过一个案例,将如上的三种通信方式都整合在一起协同使用,以下是Object
类中与线程通信相关的方法和BlockingQueue
接口的基本用法,仅供参考:
1.测试代码
package com.secf.service.action.hpy.day4;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
/**
* @Author bug菌
* @Source 公众号:猿圈奇妙屋
* @Date 2024-06-27 16:35
*/
public class ThreadCommunicationExample {
private boolean condition = false;
// 同步方法或同步块中使用wait()和notifyAll()
public synchronized void waitForCondition() throws InterruptedException {
// 等待直到condition变为true
while (!condition) {
wait();
}
// 条件满足后,可以执行后续操作
// ...
}
public synchronized void changeCondition() {
// 更改条件,并通知所有等待的线程
condition = true;
notifyAll();
}
// 使用BlockingQueue的produce方法
public void produce(BlockingQueue<Integer> queue) throws InterruptedException {
// 将元素放入队列,这里使用线程安全的put操作
// 如果队列已满,put将阻塞直到队列有空间
queue.put(1);
// 可以添加额外的逻辑,例如通知消费者
}
// 使用BlockingQueue的consume方法
public void consume(BlockingQueue<Integer> queue) throws InterruptedException {
// 从队列中取出元素,这里使用线程安全的take操作
// 如果队列为空,take将阻塞直到队列中有元素可用
int item = queue.take();
// 处理取出的元素
// ...
}
}
定义个main主函数,创建一个ThreadCommunicationExample
实例和一个LinkedBlockingQueue
实例,用于测试。
public static void main(String[] args) {
ThreadCommunicationExample example = new ThreadCommunicationExample();
BlockingQueue<Integer> queue = new LinkedBlockingQueue<>();
// 创建生产者线程
Thread producer = new Thread(() -> {
try {
example.produce(queue);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// 创建消费者线程
Thread consumer = new Thread(() -> {
try {
example.consume(queue);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
producer.start();
consumer.start();
}
2.测试结果
根据如上的测试用例,作者在本地进行测试结果如下,仅供参考,你们也可以自行修改测试用例或者添加其他的测试数据或测试方法,以便于进行熟练学习以此加深知识点的理解。
3.测试代码分析
根据如上测试用例,在此我给大家进行深入详细的捋一捋测试代码的全过程,以便于更多的同学能够加深印象并且把它吃透。
这段代码不言而喻它是一个多线程通信例子,我主要是为了演示如何在Java中使用wait()
、notifyAll()
以及BlockingQueue
来实现线程间的协调。以下是我对测试代码的详细解析,希望能够帮助大家理解:
a.类定义和成员变量
ThreadCommunicationExample
类用于演示线程通信。condition
是一个私有的布尔型成员变量,用作协调生产者和消费者线程的条件。
b.同步方法
waitForCondition()
是一个同步方法,调用它的线程将等待直到condition
变为true
。它使用wait()
方法来等待,并且当condition
为false
时,线程将阻塞。changeCondition()
也是一个同步方法,用于更改condition
的值并使用notifyAll()
唤醒所有等待该对象锁的线程。
c.线程安全队列操作
produce(BlockingQueue<Integer> queue)
方法模拟生产者的行为,向BlockingQueue
中放入元素。如果队列已满,put
方法将阻塞,直到队列中有空余空间。consume(BlockingQueue<Integer> queue)
方法模拟消费者的行为,从BlockingQueue
中取出元素。如果队列为空,take
方法将阻塞,直到队列中有元素可用。
d.主函数main
- 在
main
方法中,创建了一个ThreadCommunicationExample
实例和一个LinkedBlockingQueue
实例。 - 生产者线程在启动前会调用
waitForCondition()
等待条件满足。 - 消费者线程同样在启动前调用
waitForCondition()
等待条件满足。 - 一个额外的线程被创建来模拟条件变更,它将在1秒后调用
changeCondition()
,从而唤醒等待的线程。 - 所有线程都通过调用
start()
方法启动。
e.线程间的协调
- 生产者和消费者线程的启动顺序是关键。在这个例子中,条件变更线程先启动,以确保在生产者和消费者线程开始执行它们的任务之前,条件已经被满足。
- 一旦条件变更线程调用了
changeCondition()
,所有等待的线程(生产者和消费者)将被唤醒,并继续执行它们的任务。
f.异常处理
- 所有线程都捕获了
InterruptedException
,这是Java线程被中断时抛出的异常。捕获这个异常是好的实践,可以确保线程能够适当地响应中断。
g.代码执行流程
- 程序启动,创建
ThreadCommunicationExample
和BlockingQueue
实例。 - 创建并启动生产者、消费者和条件变更线程。
- 条件变更线程等待1秒后唤醒所有等待的线程。
- 生产者和消费者线程被唤醒,开始执行它们的任务:生产者向队列添加元素,消费者从队列取出元素。
有了我如上详细的测试用例代码解析后,同学们想必理解这期的知识点就没啥难处了吧。
4.全文小结
在本文中,我们深入探讨了Java线程间通信的机制,这是并发编程中一个至关重要的环节。通过我细致的解析和实际的代码示例,我们不仅能够理解wait()
、notify()
和notifyAll()
这些基本的线程间通信方法,还能学会如何使用BlockingQueue
来实现高效的生产者-消费者模型,简单概括如下:
1.学习小结
- 理解线程间通信的重要性:我们能够认识到线程间通信在避免竞态条件、死锁等问题中的核心作用。
- 掌握基本通信方法:通过
wait()
和notifyAll()
等方法,我们学会了如何让线程等待条件满足或进行适当的唤醒。 - 探索
BlockingQueue
的应用:我们了解到了BlockingQueue
在处理线程间协调问题时的高效性和实用性。 - ...
总之,不会白白浪费大家时间阅读毫无意义的文字的,如果有,我会积极改正。
2.应用实践
通过我写的测试案例代码,你们可以看到如何将理论知识应用于解决实际问题。从生产者-消费者模型到任务调度,再到数据缓存,BlockingQueue
证明了其在多线程环境中的广泛应用。
3.深入理解
文章中我也对wait()
和BlockingQueue
的源码进行了分析,让大家可以了解Java并发机制的内部工作方式,增强大家对这些工具背后原理的理解。
总结
总而言之,Java通过提供强大的通信机制,使得我们能够更容易地处理复杂的多线程问题。通过本文的学习和实操,无疑将加深你对Java并发编程的理解,并为你在实际开发中解决相关问题提供了坚实的基础。
文末
最后,希望本文能够成为你Java并发学习之路上的一盏明灯,照亮你前行的方向。如果你觉得本文对你有帮助,请不要吝啬你的点赞和分享,让更多的朋友能够受益。同时,也欢迎加入我们的技术社区,与志同道合的伙伴们一起成长,共同进步。
至此,感谢阅读本文,如果你觉得有所收获,不妨点赞、关注和收藏,以支持bug菌继续创作更多高质量的技术内容。同时,欢迎加入我的技术社区,一起学习和成长。
学无止境,探索无界,期待在技术的道路上与你再次相遇。咱们下期拜拜~~
往期推荐
转载自:https://juejin.cn/post/7395389351640940556