线程之间的通信(生产者与消费者模型)
1、什么是线程通信
线程通信:就是指多个线程在处理同一个资源,但是需要处理的动作(任务)不同,此时我们就需要使用到线程的通信来解决多线程之间对同一资源的使用和操作。
本文介绍的线程通信使用到三种方式:
- ①、使用等待通知机制控制线程通信(synchronized + wait + notify)
- ②、使用Condition控制线程通信(Lock + Condition + await + signal)
- ③、使用阻塞队列控制线程通信(BlockingQueue)
由于在线程通信中生产者-消费者模型是最经典的经典问题之一,所有下面就都用生产者与消费者来举例。它就像我们学习Java语言中的Hello World一样经典。
我们这里的生产者-消费者模型为:
生产者Producer不停的生产资源,将其放在仓库(缓冲池)中,也就是下面代码中的Resource,仓库最大容量为10,然后消费者不停的从仓库(缓冲池)中取出资源。当仓库(缓冲池)已装满时,生产者线程就需要停止自己的生产操作,使自己处于等待阻塞状态,并且放弃锁,让其它线程执行。因为如果生产者不释放锁的话,那么消费线程就无法消费仓库中的资源,这样仓库资源就不会减少,而且生产者就会一直无限等待下去。因此,当仓库已满时,生产者必须停止并且释放锁,这样消费者线程才能够执行,等待消费者线程消费资源,然后再通知生产者线程生产资源。同样地,当仓库为空时,消费者也必须等待,等待生产者通知它仓库中有资源了。这种互相通信的过程就是线程间的通信(协作)。
2、使用等待通知机制控制线程通信(synchronized + wait + notify)
在Java线程通信中,等待通知机制是最传统的方式,就是在一个线程进行了规定操作后,该线程就进入等待状态(wait), 等待其它线程执行完它们的指定代码过后,再将之前等待的线程唤醒(notify)。等待通知机制中使用到wait()、notify()和notifyAll()这三个方法,它们都属于Object这个类中,由于所有的类都从Object继承而来,因此,所有的类都拥有这些共有方法可供使用。而且,由于他们都被声明为final,因此在子类中不能覆写任何一个方法。
我们来看看这个三个方法的介绍:
- wait():让当前的线程进入等待阻塞状态,直到其它线程调用此对象的 notify()方法或notifyAll()方法才唤醒。
- notify():唤醒在此对象监视器(锁)上等待的单个线程(随机唤醒)。
- notifyAll():唤醒在此对象监视器(锁)上等待的所有线程。
后下面详细说明一下各个方法在使用中需要注意的几点:
首先这三个方法必须在同步方法或同步块中调用,而且必须由同步监视器(锁对象)来调用,并且它们的同步监视器(锁对象)必须一致。
1、wait()
public final void wait() throws InterruptedException , IllegalMonitorStateException
该方法用来将当前线程置入休眠(阻塞)状态,直到接到通知或被中断为止。在调用wait()之前,线程必须要获得该对象的对象级别锁,即只能在同步方法或同步块中调用wait()方法。进入wait()方法后,当前线程释放锁。在从wait()返回前,线程与其他线程竞争重新获得锁。如果调用wait()时,没有持有适当的锁,则抛出IllegalMonitorStateException。
2、notify()
public final native void notify() throws IllegalMonitorStateException
该方法也要在同步方法或同步块中调用,即在调用前,线程也必须要获得该对象的对象级别锁,如果调用notify()时没有持有适当的锁,也会抛出IllegalMonitorStateException。
该方法用来通知那些可能等待该对象的对象锁的其他线程。如果有多个线程等待,则线程规划器任意挑选出其中一个wait()状态的线程来发出通知,并使它等待获取该对象的对象锁,但不惊动其他同样在等待被该对象notify的线程们。当第一个获得了该对象锁的wait线程运行完毕以后,它会释放掉该对象锁,此时如果该对象没有再次使用notify语句,则即便该对象已经空闲,其他wait状态等待的线程由于没有得到该对象的通知,会继续阻塞在wait状态,直到这个对象发出一个notify或notifyAll。这里需要注意:它们等待的是被notify或notifyAll,而不是锁。这与下面的notifyAll()方法执行后的情况不同。
特别注意:当在同步中调用wait()方法时,执行该代码的线程会立即放弃它在对象上的锁。然而在调用notify()时,并不意味着这时线程会放弃该对象锁,而是要等到程序运行完synchronized代码块后,当前线程才会释放锁,wait所在的线程也才可以获取该对象锁。
3、notifyAll()
public final native void notifyAll() throws IllegalMonitorStateException
该方法与notify()方法的工作方式相同,重要的一点差异是:
notifyAll使所有原来在该对象上wait的线程统统退出wait的状态(即全部被唤醒,不再等待notify或notifyAll,但由于此时还没有获取到该对象锁,因此还不能继续往下执行),变成等待获取该对象上的锁,一旦该对象锁被释放(notifyAll线程退出调用了notifyAll的synchronized代码块的时候),他们就会去竞争。如果其中一个线程获得了该对象锁,它就会继续往下执行,在它退出synchronized代码块,释放锁后,其它的已经被唤醒的线程将会继续竞争获取该锁,一直进行下去,直到所有被唤醒的线程都执行完毕。
上面BB了这么多,下面举一个生产者-消费者的栗子应该就明白了:
package com.thr;
/**
* @author Administrator
* @date 2020-04-02
* @desc synchronized+wait+notify线程通信举例(生产者消费者模型)
*/
public class ProducerConsumerWaitNotify {
public static void main(String[] args) {
Resource resource = new Resource();
//创建3个生产者线程
Thread t1 = new Thread(new Producer(resource),"生产线程1");
Thread t2 = new Thread(new Producer(resource),"生产线程2");
Thread t3 = new Thread(new Producer(resource),"生产线程3");
//创建2个消费者线程
Thread t4 = new Thread(new Consumer(resource),"消费线程1");
Thread t5 = new Thread(new Consumer(resource),"消费线程2");
//生产者线程启动
t1.start();
t2.start();
t3.start();
//消费者线程启动
t4.start();
t5.start();
}
}
/**
* 共享资源(仓库)
*/
class Resource{
//当前资源数量
private int num = 0;
//最大资源数量
private int size = 10;
//生产资源
public synchronized void add(){
if (num < size){
num++;
System.out.println("生产者--" + Thread.currentThread().getName() +
"--生产一件资源,当前资源池有" + num + "个");
notifyAll();
}else{
try {
wait();
System.out.println("生产者--"+Thread.currentThread().getName()+"进入等待状态,等待通知");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//消费资源
public synchronized void remove(){
if (num > 0){
num--;
System.out.println("消费者--" + Thread.currentThread().getName() +
"--消耗一件资源," + "当前线程池有" + num + "个");
notifyAll();
}else{
try {
wait();
System.out.println("消费者--" + Thread.currentThread().getName() + "进入等待状态,等待通知");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* 生产者线程
*/
class Producer implements Runnable{
//共享资源对象
private Resource resource;
public Producer(Resource resource) {
this.resource = resource;
}
@Override
public void run() {
while (true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
resource.add();
}
}
}
/**
* 消费者线程
*/
class Consumer implements Runnable{
//共享资源对象
private Resource resource;
public Consumer(Resource resource) {
this.resource = resource;
}
@Override
public void run() {
while (true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
resource.remove();
}
}
}
运行的结果如下:
实际上生产者-消费者模型中更应该加一个"仓库”,因为该模型离开了“仓库”生产者消费者模型就显得没有说服力了。而对于此模型,应该明确一下几点:
- 生产者仅仅在仓库未满时候生产,仓库满则停止生产。
- 消费者仅仅在仓库有产品时候才能消费,仓库空则等待。
- 当消费者发现仓库没产品可消费时候会通知生产者生产。
- 生产者在生产出可消费产品时候,应该通知等待的消费者去消费。
再来比较一下sleep() 和 wait()的异同?
相同点:都可以使得当前的线程进入阻塞状态。
不同点:
- ①、声明的位置不同:Thread类中声明sleep() , Object类中声明wait()。
- ②、调用的要求不同:sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中。
- ③、是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。
- ④、如何唤醒:sleep()自动唤醒,wait()需要手动调用notify()和notifyAll()。
3、使用Condition控制线程通信(Lock + Condition + await + signal)
Condition是在Java 1.5中才出现的,它是一个接口,其内部基本的方法就是await()、signal()和signalAll()方法。它的作用就是用来代替传统的Object类中的wait()、notify()实现线程间的通信。相比使用Object的wait()、notify(),使用Condition的await()、signal()这种方式实现线程间协作更加安全和高效,因此通常来说比较推荐使用Condition。但是需要注意的是Condition依赖于Lock接口,它必须在Lock实例中使用,否则会抛出IllegalMonitorStateException。也就是说调用Condition的await()和signal()方法,必须在lock.lock()和lock.unlock之间才可以使用。
- Conditon中的await()对应Object的wait();
- Condition中的signal()对应Object的notify();
- Condition中的signalAll()对应Object的notifyAll()。
Condition对象的生成代码如下:
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
使用Condition控制线程通信举例:
package com.thr;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author Administrator
* @date 2020-04-03
* @desc Lock + Condition + await + signal举例
*/
public class ProducerConsumerCondition {
public static void main(String[] args) {
//创建ReentrantLock和Condition对象
Lock lock = new ReentrantLock();
Condition producerCondition = lock.newCondition();
Condition consumerCondition = lock.newCondition();
Resource resource = new Resource(lock,producerCondition,consumerCondition);
//创建3个生产者线程
Thread t1 = new Thread(new Producer(resource),"生产线程1");
Thread t2 = new Thread(new Producer(resource),"生产线程2");
Thread t3 = new Thread(new Producer(resource),"生产线程3");
//创建2个消费者线程
Thread t4 = new Thread(new Consumer(resource),"消费线程1");
Thread t5 = new Thread(new Consumer(resource),"消费线程2");
//生产者线程启动
t1.start();
t2.start();
t3.start();
//消费者线程启动
t4.start();
t5.start();
}
}
/**
* 共享资源(仓库)
*/
class Resource{
//当前资源数量
private int num = 0;
//最大资源数量
private int size = 10;
//定义lock和condition
private Lock lock;
private Condition producerCondition;
private Condition consumerCondition;
//初始化
public Resource(Lock lock, Condition producerCondition, Condition consumerCondition) {
this.lock = lock;
this.producerCondition = producerCondition;
this.consumerCondition = consumerCondition;
}
//生产资源
public void add(){
lock.lock();//获取锁
try {
if (num < size){
num++;
System.out.println("生产者--" + Thread.currentThread().getName() +
"--生产一件资源,当前资源池有" + num + "个");
//唤醒等待的消费者
consumerCondition.signalAll();
//notifyAll();
}else{
//让生产者线程等待
producerCondition.await();
//wait();
System.out.println("生产者--"+Thread.currentThread().getName()+"进入等待状态,等待通知");
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();//释放锁
}
}
//消费资源
public void remove(){
lock.lock();//获取锁
try {
if (num > 0){
num--;
System.out.println("消费者--" + Thread.currentThread().getName() +
"--消耗一件资源," + "当前线程池有" + num + "个");
//唤醒等待的生产者
producerCondition.signalAll();
//notifyAll();
}else{
//让消费者线程等待
consumerCondition.await();
//wait();
System.out.println("消费者--" + Thread.currentThread().getName() + "进入等待状态,等待通知");
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();//释放锁
}
}
}
/**
* 生产者线程
*/
class Producer implements Runnable{
//共享资源对象
private Resource resource;
public Producer(Resource resource) {
this.resource = resource;
}
@Override
public void run() {
while (true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
resource.add();
}
}
}
/**
* 消费者线程
*/
class Consumer implements Runnable{
//共享资源对象
private Resource resource;
public Consumer(Resource resource) {
this.resource = resource;
}
@Override
public void run() {
while (true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
resource.remove();
}
}
}
实现的效果跟上面wait和notify是一样的。
4、使用BlockingQueue控制线程通信(BlockingQueue)
BlockingQueue也是在Java 1.5中才出现的,是一个接口,它继承了Queue接口。BlockingQueue的底层实际上是使用了Lock和Condition来实现的,它帮我们搞好了一切(已经帮我们调用了lock、unlock、await和signal方法),我们只需调用BlockingQueue中合适的方法即可,所以使用BlockingQueue可以很轻松的实现线程通信。
BlockingQueue具有的特征:如果该队列已满,当生产者线程试图向BlockingQueue中放入元素时,则线程被阻塞。如果队列已空,消费者线程试图从BlockingQueue中取出元素时,则该线程阻塞。
BlockingQueue提供如下两个支持阻塞的方法:
- (1) put(E e):尝试把元素放入BlockingQueue的尾部,如果该队列的元素已满,则阻塞该线程。
- (2) take():尝试从BlockingQueue的头部取出元素,如果该队列的元素已空,则阻塞该线程。
BlockingQueue既然继承了Queue接口,那当然当然也可以使用Queue接口中的方法,这些方法归纳起来可以分为如下三组:
- (1) 在队列尾部插入元素,包括add(E e)、offer(E e)、put(E e)方法,当该队列已满时,这三个方法分别会抛出异常、返回false、阻塞队列。
- (2) 在队列头部删除并返回删除的元素。包括remove()、poll()、和take()方法,当该队列已空时,这三个方法分别会抛出异常、返回false、阻塞队列。
- (3) 在队列头部取出但不删除元素。包括element()和peek()方法,当队列已空时,这两个方法分别抛出异常、返回false。
BlockingQueue最终会有四种状况,抛出异常、返回特殊值(常常是 true / false)、阻塞、超时,下表总结了这些方法:
位置 | 抛出异常 | 返回特殊值 | 阻塞 | 超时 |
---|---|---|---|---|
插入 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
移除 | remove() | poll() | take() | poll(time,unit) |
检查 | element() | peek() | 不可用 | 不可用 |
BlockingQueue是个接口,它有如下5实现类:
- ArrayBlockingQueue(数组阻塞队列):基于数组实现的有界BlockingQueue队列,按FIFO(先进先出)原则对元素进行排序,创建其对象必须明确大小。
- LinkedBlockingQueue(链表阻塞队列):基于链表实现的BlockingQueue队列,按FIFO(先进先出)原则对元素进行排序,建其对象如果没有明确大小,默认值是Integer.MAX_VALUE。
- PriorityBlockingQueue(优先级阻塞队列):它并不是按FIFO(先进先出)原则对元素进行排序,该队列调用remove()、poll()、take()等方法提取出元素时,是根据对象(实现Comparable接口)的本身大小来自然排序或者Comparator来进行定制排序的。不懂排序的可以查看这篇博客:夯实Java基础(十五)——Java中Comparable和Comparator
- SynchronousQueue(同步阻塞队列):它是一个特殊的BlockingQueue,其内部同时只能够容纳单个元素。如果该队列已有一元素的话,尝试向队列中插入一个新元素的线程将会阻塞,直到另一个线程将该元素从队列中抽走。同样,如果该队列为空,试图向队列中抽取一个元素的线程将会阻塞,直到另一个线程向队列中插入了一条新的元素。
- DelayQueue(延迟阻塞队列):它也是一个特殊的BlockingQueue,底层基于PriorityBlockingQueue实现,不过,DelayQueue要求集合元素都实现Delay接口(该接口里只有一个long getDelay()方法), DelayQueue根据集合元素的getDalay()方法的返回值进行排序。
使用BlockingQueue控制线程通信举例:
package com.thr;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
/**
* @author Administrator
* @date 2020-04-03
* @desc BlockingQueue线程通信举例
*/
public class ProducerConsumerBlockingQueue {
public static void main(String[] args) {
Resource resource = new Resource();
//创建3个生产者线程
Thread t1 = new Thread(new Producer(resource),"生产线程1");
Thread t2 = new Thread(new Producer(resource),"生产线程2");
Thread t3 = new Thread(new Producer(resource),"生产线程3");
//创建2个消费者线程
Thread t4 = new Thread(new Consumer(resource),"消费线程1");
Thread t5 = new Thread(new Consumer(resource),"消费线程2");
//生产者线程启动
t1.start();
t2.start();
t3.start();
//消费者线程启动
t4.start();
t5.start();
}
}
/**
* 共享资源(仓库)
*/
class Resource{
//定义一个链表队列,最大容量为10
private BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue(10);
//生产资源
public void add(){
try {
blockingQueue.put(1);
System.out.println("生产者--" + Thread.currentThread().getName() +
"--生产一件资源,当前资源池有" + blockingQueue.size() + "个");
} catch (Exception e) {
e.printStackTrace();
}
}
//消费资源
public void remove(){
try {
blockingQueue.take();
System.out.println("消费者--" + Thread.currentThread().getName() +
"--消耗一件资源," + "当前线程池有" + blockingQueue.size() + "个");
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 生产者线程
*/
class Producer implements Runnable{
//共享资源对象
private Resource resource;
public Producer(Resource resource) {
this.resource = resource;
}
@Override
public void run() {
while (true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
resource.add();
}
}
}
/**
* 消费者线程
*/
class Consumer implements Runnable{
//共享资源对象
private Resource resource;
public Consumer(Resource resource) {
this.resource = resource;
}
@Override
public void run() {
while (true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
resource.remove();
}
}
}
参考资料:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 通过 API 将Deepseek响应流式内容输出到前端
· 因为Apifox不支持离线,我果断选择了Apipost!