JUC——并发容器BlockingQueue数据共享通道源码解析

1:BlockingQueue数据共享通道介绍

对于并发程序而言,高性能自然是个我们需要追求的目标,但多线程的开发模式还会引入一个问题,那就是如何进行多个线程间的数据共享呢?比如,线程A希望给线程B发一条消息,用什么方式告知线程B是比较合理的呢?

一般来说,我们总是希望整个系统是松散耦合的。比如,你所在学校希望可以得到些学生的意见, 设立了一个意见箱,如果对学校有任何要求或者意见都可以投到意见箱里。作为学生的你并不需要直接找到学校相关的工作人员就能表达意见。实际上,学校的工作人员也可能经常发生变动,直接找工作人员未必是一件方便的事情。 而你投递到意见前的意见总是会被学校的工作人员看到,不管是否发生了人员的变动。这样你就可以很容易表达自己的诉求了。你既不需要直接和他们对话,又可以轻松提出自己的建议。

就是说我们既希望线程A能够通知线程B,又希望线程A不知道线程B的存在。这样,如果将来进行重构成者升级,我们完全可以不修改线程A,而直接把线程B升级为线程C,保证系统的平滑过度。而这中间的“意见箱”就可以使用BlockingQueue来实现。

BlockingQueue是一个接口, 并非一个具体的实现。它的主要实现有下面一些
在这里插入图片描述
在这里插入图片描述

  1. ArrayBlockingQueue:由数组结构组成的有界阻塞队列。
  2. LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为integer.MAX_VALUE)阻塞队列。
  3. PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
  4. DelayQueue:使用优先级队列实现的延迟无界阻塞队列。
  5. SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列。
  6. LinkedTransferQueue:由链表组成的无界阻塞队列。
  7. LinkedBlocking Deque:由链表组成的双向阻塞队列。

这里我们主要介绍ArrayBlockingQueue类和LinkedBlockingQueue类,从名字应该可以得知,ArrayBlockingQueue类是基于数组实现的,而LinkedBlockingQueue类是基于链表的。也正因为如此,ArrayBlockingQueue类更适合做有界队列因为队列中可容纳的最大元素需要在队列创建时指定(毕竟数组的动态扩展不太方便)。而LinkedBlockingQueue类适合做无界队列,或者那些边界值非常大的队列,因为其内部元素可以动态增加,它不会因为初值容量很大,而占据一大半的内存。

在这里插入图片描述
当队列是空的,从队列中获取元素的操作将会被阻寒。当队列是满的,从队列中添加元素的操作将会被阻塞。

试图从空的队列中获取元素的线程将会被阻塞,直到其他线程往空的队列插入新的元素
试图向已满的队列中添加新元素的线程将会被阻塞,直到其他线程从队列中移除一个或多个元素或者完全清空,使队列变得空闲起来并后续新增

2:BlockingQueue的用处

在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤起。

为什么需要BlockingQueue?
好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一手包办了

在concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。

3:BlockingQueue的核心方法

在这里插入图片描述
在这里插入图片描述

4:BlockingQueue工作机制——源码解析

BlockingQueue之所以适合作为数据共享的通道其关键还在于Blocking上,Blocking是阻塞的意思,当服务线程(服务线程指不断获取队列中的消息,进行处理的线程)处理完成队列中所有的消息后,它如何知道下一条消息何时到来呢?

一种最简 单的做法是让这个线程按照一定的时间间隔不停地循环和监控这个队列,这是一种可行方案是,但显然造成了 不必要的资源浪费,而且循环周期也难以确定。**BlockingQueue很好地解决了这个问题,它会让服务线程在队列为空时进行等待。当有新的消息进入队列后,自动将线程唤醒。**那它是如何实现的呢?我们以ArrayBlockingQueue类为例,来一探究竟。

BlockingQueue工作模式
在这里插入图片描述

ArrayBlockingQueue类的内部元素都放置在一个对象数组中:
在这里插入图片描述

向队列中压入元素,我们需要关注的是put()方法。put()方法也是将元素压入队列末尾。但如果队列满了,它会一直等待, 直到队列中有空闲的位置。

从队列中弹出元素,我们需要关注的是take()方法, 而take()方法会等待,直到队列内有可用元素。

因此,put()方法和take()方法才是体现Blocking的关键。为了做好等待和通知两件事,在ArayBlockingQueue类内部定义了以下一些字段。
在这里插入图片描述
take方法分析如下
在这里插入图片描述
put方法分析如下

在这里插入图片描述

从实现上说,ArayBlockingQueue类在物理上是一个数组,但在逻辑层面是一个环形结构。由于其数组的特性,其容量大小在初始化时就已经指定,并且无法动态调整。当有元素加入或者离开ArrayBlockingQueue类时,总是使用takeIndex和putIndex两个变量分别表示队列头部和尾部元素在数组中的位置。每一次入队和出队操作都会调整这两个重要的索引位置。
在ArayBlockingQueue类内部定义了以下字段。
在这里插入图片描述
在这里插入图片描述

5:BlockingQueue实现生产者消费者模式

BlockingQueue的使用非常普遍。生产者消费者模式中,我们可以更清楚地看到如何使用BlockingQueue解耦生产者和消费者

生产者和消费者案例演示

class Product{
2   private ArrayBlockingQueue<Object> objects = new ArrayBlockingQueue<>(3);
3   private ReentrantLock reentrantLock=new ReentrantLock();
4   Condition condition=reentrantLock.newCondition();
   public void produce(int i) throws InterruptedException {
       reentrantLock.lock();
       try{
           while (objects.size()==3){
               condition.await();
           }
           System.out.println(Thread.currentThread().getName()+"生产产品---"+i);
           objects.put(i);
           System.out.println(Thread.currentThread().getName()+"生产产品完成---"+i);
           condition.signalAll();
       }catch(Exception e){
          e.printStackTrace();
       }finally{
           reentrantLock.unlock();
       }
   }
    public void consume(int i) throws InterruptedException {
       reentrantLock.lock();
       try{
           while (objects.size()==0){
               condition.await();
           }
           System.out.println(Thread.currentThread().getName()+"消费产品---");
           Object take = objects.take();
           System.out.println(Thread.currentThread().getName()+"消费产品完成---"+take);
           condition.signalAll();
       }catch(Exception e){
          e.printStackTrace();
       }finally{
           reentrantLock.unlock();
       }
    }
}
public class BlockingQueueProAndCon {
    public static void main(String[] args) throws InterruptedException {
        Product product = new Product();
        //模拟生产者生产商品
        for (int i=0;i<6;i++){
            final int inttempt=i;
            new Thread(()->{
                try {
                    product.produce(inttempt);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
        //模拟消费者消费
        for (int i=0;i<6;i++){
            final int inttempt=i;
            new Thread(()->{
                try {
                    product.consume(inttempt);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
    }
}
0生产产品---0
0生产产品完成---0
0消费产品---
0消费产品完成---0
3生产产品---3
3生产产品完成---3
//..
2消费产品---
2消费产品完成---3
//...

上面代码第2行使用BlockingQueue来作为生产者和消费者的内存缓冲区,3,4行知道还是要使用锁和阻塞等待来实现线程间的同步

使用BlockingQueue队列实现生产者和消费者是一个不错的选择。它可以很自然地实现作为生产者和消费者的内存缓冲区。但是BlockingQueue并不是一个高性能的实现,它完全使用锁和阻塞等待来实现线程间的同步。在并发场合下,它的性能并不是特别的优越

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值