并发编程系列之阻塞队列(BlockingQueue)

本人花费半年的时间总结的《Java面试指南》已拿腾讯等大厂offer,已开源在github ,欢迎star!

转载声明:转载请注明出处,本技术博客是本人原创文章

本文GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了6个月总结的一线大厂Java面试总结,本人已拿大厂offer,欢迎star

原文链接:blog.ouyangsihai.cn >> 并发编程系列之阻塞队列(BlockingQueue)

并发编程系列之阻塞队列(BlockingQueue)

前言

上节我们介绍了非阻塞队列ConcurrentLinkedQueue的相关内容,今天我们再来说说Java中的阻塞队列BlockingQueue,主要介绍下阻塞队列的概念,常见的阻塞队列,以及阻塞队列的底层实现。

并发编程系列之阻塞队列(BlockingQueue)

什么是阻塞队列?

阻塞队列就是一种支持阻塞的插入和移除操作的特殊容器

  • 阻塞的插入:当队列满时,向队列中插入元素的线程会被阻塞,直到队列中有元素被移除,即队列不满时,阻塞的线程才能继续向队列中插入元素;
  • 阻塞的移除:当队列中没有元素时,即队列为空时,从队列中移除元素的线程就会被阻塞,直到队列中有新的元素被添加,即队列中有元素时,阻塞的线程才能继续从队列中移除元素;
  • 阻塞的移除:当队列中没有元素时,即队列为空时,从队列中移除元素的线程就会被阻塞,直到队列中有新的元素被添加,即队列中有元素时,阻塞的线程才能继续从队列中移除元素;

    阻塞队列的常见操作如下:

    并发编程系列之阻塞队列(BlockingQueue) 并发编程系列之阻塞队列(BlockingQueue)

    常见的几种阻塞队列

    BlockingQueue是一个接口,主要有下面7种实现类

  • ArrayBlockingQueue:基于数组的阻塞队列实现,在其内部,维护了一个定长数组,以便缓存队列中的数据对象,其内部没实现读写分离,也就意味着生产和消费不能完全并行,长度是需要自己定义的,可以指定先进先出或者先进后出,也被称为“有界队列”
  • LinkedBlockingQueue:基于链表的阻塞队列,跟ArrayBlockingQueue类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成),LinkedBlockingQueue之所以能够高效的处理并发数据,是因为其内部实现采用分离锁(读写分离两个锁),从而实现生产者和消费者操作完全并发执行,也是一个“无界队列”
  • PriorityBlockingQueue:基于优先级的阻塞队列(优先级的判断通过构造函数传入的Compator(比较器)对象决定,也就是说传入队列的对象必须实现Comparable接口),在实现PriorityBlockingQueue时,内部控制线程同步的锁采用的是公平锁,是一个“无界队列”(PriorityBlockingQueue调用take后需要重新排序,调一次重新排一次)
  • DelayQueue:带有延迟时间的无界阻塞Queue,其中的元素只有当指定的延迟时间到了,才能够从队列中获取该元素。DelayQueue中的元素必须实现Delayed接口,DelayQueue是一个没有大小限制的队列,应用场景比较多,比如对缓存超时的数据进行移除,任务超时处理,空间连接的关闭等等
  • SynchronousQueue:不存储任何元素的队列,生产者产生的数据直接会被消费者获取并消费,即没一个put操作必须等待一个take操作,否则不能继续添加元素,或者你可以理解为是只能存储一个元素的队列,存一个就满了,该元素必须被移除掉,才能继续添加
  • LinkedTransferQueue:一个由链表结构组成的无界阻塞传输队列,主要体现在LinkedTransferQueue多2个方法:
    • transfer(E):如果当前有消费者正在等待消费,则生产者直接把元素传输给消费者,如果当前没有消费者正在等待消费,则生产者将元素存放在队列的tail节点上,并等到该元素被消费才返回(采用自旋等待);
    • tryTransfer(E):将元素立刻传输给一个等待接收元素的线程,如果没有消费者就会返回false,而不将元素放入队列;
    • tryTransfer(E,long,TimeUnit):将元素立刻给消费者,如果没有消费者就等待指定时间。时间到时,如果还没有消费者则失败返回false;
    • LinkedBlockingQueue:基于链表的阻塞队列,跟ArrayBlockingQueue类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成),LinkedBlockingQueue之所以能够高效的处理并发数据,是因为其内部实现采用分离锁(读写分离两个锁),从而实现生产者和消费者操作完全并发执行,也是一个“无界队列”

      DelayQueue:带有延迟时间的无界阻塞Queue,其中的元素只有当指定的延迟时间到了,才能够从队列中获取该元素。DelayQueue中的元素必须实现Delayed接口,DelayQueue是一个没有大小限制的队列,应用场景比较多,比如对缓存超时的数据进行移除,任务超时处理,空间连接的关闭等等

      LinkedTransferQueue:一个由链表结构组成的无界阻塞传输队列,主要体现在LinkedTransferQueue多2个方法:

      tryTransfer(E):将元素立刻传输给一个等待接收元素的线程,如果没有消费者就会返回false,而不将元素放入队列;

      LinkedBlockingDeque:由链表结构组成的双向阻塞队列,即可以从队列的两端插入和移出元素

      并发编程系列之阻塞队列(BlockingQueue)

      阻塞队列的底层实现

      阻塞队列的底层主要使用的还是之前我们介绍过得等待通知机制来实现的,等待通知机制在阻塞队列中具体体现为如下思想:当生产者往一个满队列中添加元素时,生产者会被阻塞,当消费者从该队列中消费了一个元素后,会通知阻塞的插入操作的生产者线程,告诉它当前队列不满,可以继续执行添加操作。我们通过下面源码可以更好的理解这一点:

      
      final ReentrantLock lock;
      /** Condition for waiting takes */
      private final Condition notEmpty;
      /** Condition for waiting puts */
      private final Condition notFull;
      
       public ArrayBlockingQueue(int capacity, boolean fair) {
              if (capacity = 0)
                  throw new IllegalArgumentException();
              this.items = new Object[capacity];
              lock = new ReentrantLock(fair);
              // 使用condition模式等待通知
              notEmpty = lock.newCondition();
              notFull =  lock.newCondition();
          }
      
          public void put(E e) throws InterruptedException {
              checkNotNull(e);
              final ReentrantLock lock = this.lock;
              lock.lockInterruptibly();
              try {
                  while (count == items.length)
                      // 当队列满时,阻塞
                      notFull.await();
                  // 否则继续添加元素
                  insert(e);
              } finally {
                  lock.unlock();
              }
          }
      
      private void insert(E x) {
              items[putIndex] = x;
              putIndex = inc(putIndex);
              ++count;
              // 添加元素时会唤醒等待移出数据的take线程
              notEmpty.signal();
          }
      
      public E take() throws InterruptedException {
              final ReentrantLock lock = this.lock;
              lock.lockInterruptibly();
              try {
                  while (count == 0)
                      // 当队列为空时,获取数据的线程等待
                      notEmpty.await();
                  // 否则就取出元素,并且唤醒等待的put线程
                  return extract();
              } finally {
                  lock.unlock();
              }
          }
      
          private E extract() {
              final Object[] items = this.items;
              E x = this.Ecast(items[takeIndex]);
              items[takeIndex] = null;
              takeIndex = inc(takeIndex);
              --count;
              // 唤醒等待的put线程
              notFull.signal();
              return x;
          }
      

      相关文章:

      并发编程系列之阻塞队列(BlockingQueue)

      原文始发于微信公众号(Justin的后端书架):

    本人花费半年的时间总结的《Java面试指南》已拿腾讯等大厂offer,已开源在github ,欢迎star!

    转载声明:转载请注明出处,本技术博客是本人原创文章

    本文GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了6个月总结的一线大厂Java面试总结,本人已拿大厂offer,欢迎star

    原文链接:blog.ouyangsihai.cn >> 并发编程系列之阻塞队列(BlockingQueue)


     上一篇
    并发编程系列之Fork,Join 并发编程系列之Fork,Join
    前言 上节我们讲了阻塞队列,Java中的并发容器就算有了个基本的认识,今天我们来介绍一种线程工作模式,叫Fork/Join,他是JDK7之后提供的一个并行执行框架,主要的思想我觉得是分而治之,将一个大的任务分成多个小的任务并
    2021-04-05
    下一篇 
    并发编程系列之并发容器——ConcurrentLinkedQueue 并发编程系列之并发容器——ConcurrentLinkedQueue
    前言 上节我们介绍了线程安全的HashMap,今天我们再来介绍一个线程安全的并发容器:ConcurrentLinkedQueue,它是一个线程安全的队列,在Java中如果要实现一个线程安全的队列由2种方式:一个是使用阻塞算法的队列,用
    2021-04-05