阻塞队列--BlockingQueue

前言

之前写深入理解Java线程池-ThreadPoolExecutor实现原理(一)这篇文章时,ThreadPoolExecutor中使用的BlockingQueue作缓冲存储任务,任务管理充当生产者从阻塞队列中添加任务,线程管理充当消费者从阻塞队列中获取任务,本质上构建成了一个生产-消费者模型 ,将线程任务解耦。

所以,趁热打铁,来分析下BlockingQueue和它主要实现类。

阻塞队列是什么

A {@link java.util.Queue} that additionally supports operations that wait for the queue to become non-empty when retrieving an element, and wait for space to become available in the queue when storing an element.

阻塞队列是一种支持两种额外操作的队列:

  • 队列为空时,获取元素的线程等待队列变为非空(从队列获取元素阻塞)
  • 队列为满时,存储元素的线程等待队列变为可用(往队列添加元素阻塞)

阻塞队列通过锁实现线程安全 的,元素在入队和出队获取了锁资源才能执行操作,这是一种阻塞机制 。JDK也提供非阻塞机制 实现的队列,例如:ConcurrentLinkedQueue,基于CAS方式实现。

为什么要使用阻塞队列

{@code BlockingQueue} implementations are designed to be used primarily for producer-consumer queues

阻塞队列主要用于生产-消费者 队列,使得生产者(入队)和消费者(出队)没有依赖关系,进行解耦 。同时队列作为中间缓冲,可以平衡生产者和消费者的处理能力,当生产者效率高于消费者时,还能起到削峰填谷的作用。

阻塞队列保护了共享资源,避免对共享资源的竞争冲突。

接口

我们基于JDK1.8来分析BlockingQueue,首先查看UML类图,了解继承关系。

Queue

Queue是按FIFO规则保存元素的集合,该接口提供的方法:插入、移除和检索都有两种形式,1.操作失败时抛出异常;2.返回特殊值(null或false)。

抛出异常 返回特定值
Insert add(e) offer(e)
Remove remove() poll()
Examine element() peek()

Queue的接口方法:

java 复制代码
public interface Queue<E> extends Collection<E> {
    boolean add(E e);

    boolean offer(E e);

    E remove();

    E poll();

    E element();

    E peek();
}

BlockingQueue

BlockingQueueQueue接口基础上增加了阻塞方法,1.有最大时间限制的阻塞;2.无期限阻塞。

抛出异常 返回特定值 阻塞 阻塞特定时长
Insert add(e) offer(e) put(e) offer(e,time,unit)
Remove remove() poll() take() poll(time,unit)
Examine element() peek() / /

前面提到了通过BlockingQueue构建生产-消费者,JDK中给出了一个简单示例:

java 复制代码
//生产者
class Producer implements Runnable {
    private final BlockingQueue queue;
    Producer(BlockingQueue q) { queue = q; }
    public void run() {
      try {
        while (true) { queue.put(produce()); }
      } catch (InterruptedException ex) { ... handle ...}
    }
    Object produce() { ... }
  }

//消费者
class Consumer implements Runnable {
    private final BlockingQueue queue;
    Consumer(BlockingQueue q) { queue = q; }
    public void run() {
      try {
        while (true) { consume(queue.take()); }
      } catch (InterruptedException ex) { ... handle ...}
    }
    void consume(Object x) { ... }
  }

//主方法
class Setup {
    void main() {
      BlockingQueue q = new SomeQueueImplementation();
      Producer p = new Producer(q);
      Consumer c1 = new Consumer(q);
      Consumer c2 = new Consumer(q);
      new Thread(p).start();
      new Thread(c1).start();
      new Thread(c2).start();
    }
  }

注:BlockingQueue不支持任何形式的closeshutdown来表明不能添加元素了。这些特性往往依赖于实现,例如,生产者插入一个结束标识毒丸对象,当消费者消费到这些特殊标识时,就做出相应处理。

实现类

BlockingQueue主要有6个实现类:

实现类 功能
ArrayBlockingQueue 由数组结构组成的有界阻塞队列,按FIFO(先进先出)规则对元素进行排序
LinkedBlockingQueue 由链表结构组成的有界阻塞队列,按FIFO(先进先出)规则对元素进行排序,队列默认长度Integer.MAX_VALUE
PriorityBlockingQueue 支持优先级排序的无界阻塞队列,与类PriorityQueue排序规则相同
DelayQueue 使用PriorityQueue实现的延迟无界阻塞队列,其中的元素只能在其延迟过期时被获取
SynchronousQueue 不存储元素的阻塞队列
LinkedTransferQueue 由链表结构组成的无界阻塞队列

接下来具体分析下实现类是如何实现

LinkedBlockingQueue

我们已经了解到BlockingQueue主要功能点,从功能点切入分析:

  1. 插入元素
  2. 移除元素
  3. 获取元素

同时关注插入/移除元素时如何实现并发安全,队列为空/满时如何阻塞获取/插入操作。

属性

java 复制代码
//链表节点
static class Node<E> {
    E item;

    Node<E> next;

    Node(E x) { item = x; }
}

//队列的容量,Integer.MAX_VALUE if none
private final int capacity;

//当前数量
private final AtomicInteger count = new AtomicInteger();

//头节点
transient Node<E> head;

//尾结点
private transient Node<E> last;

//take, poll等移除元素方法持有的锁
private final ReentrantLock takeLock = new ReentrantLock();

//等待队列出队条件
private final Condition notEmpty = takeLock.newCondition();

//put, offer等插入元素方法持有的锁
private final ReentrantLock putLock = new ReentrantLock();

//等待队列入队条件
private final Condition notFull = putLock.newCondition();

LinkedBlockingQueue内部是基于有头尾节点的单向链表 实现的,使用两个变量记录了队列当前数量和初始容量。针对插入元素移除元素 操作各有基于AQS的一个可重入锁ReentrantLockCondition控制,Condition是等待/通知模式的实现,类似于object.await、object.notify,但功能更强大。

构造方法

java 复制代码
public LinkedBlockingQueue() {
    //无参,默认为Integer.MAX_VALUE
    this(Integer.MAX_VALUE);
}
//指定容量
public LinkedBlockingQueue(int capacity) {
    if (capacity <= 0) throw new IllegalArgumentException();
    this.capacity = capacity;
    //头尾节点初始化
    last = head = new Node<E>(null);
}
//指定集合初始化
public LinkedBlockingQueue(Collection<? extends E> c) {
    //容量:Integer.MAX_VALUE
    this(Integer.MAX_VALUE);
    ...
}

LinkedBlockingQueue可以初始化时可以指定队列容量,若未指定则队列默认容量为Integer.MAX_VALUE

插入元素

java 复制代码
public void put(E e) throws InterruptedException {
    if (e == null) throw new NullPointerException();
    //负数,区分入队是否成功
    int c = -1;
    //创建新节点
    Node<E> node = new Node<E>(e);
    final ReentrantLock putLock = this.putLock;
    final AtomicInteger count = this.count;
    //可中断形式获取putLock
    putLock.lockInterruptibly();
    try {
        //无限阻塞,直到队列非满(队列未满时,put无限期阻塞)
        while (count.get() == capacity) {
            //入列等待,直到唤醒或中断
            notFull.await();
        }
        //节点置入队列
        enqueue(node);
        //数量+1
        c = count.getAndIncrement();
        if (c + 1 < capacity)
            //唤醒一个入队线程
            notFull.signal();
    } finally {
        putLock.unlock();
    }
    if (c == 0)
        //c==0,队列从空变为非空,则唤醒一个出队线程
        signalNotEmpty();
}

流程图:

在获取锁成功后,如果队列已满,则调用notFull.await()阻塞。当有满足下列情形之一时,会执行notFull.signal()唤醒一个等待线程,被唤醒的入列线程会继续判断while条件,当队列不满时继续入队操作。

  • 移除元素使得队列从满变为未满时
  • 新增元素增加节点后队列未满

其他插入元素方法add(e)、offer(e)、offer(e,time,unit)put(e)大体一致,在方法自身特性是否阻塞返回特定值或抛出异常方面有细微差异,不在一一细讲。

移除元素

java 复制代码
public E take() throws InterruptedException {
    E x;
    int c = -1;
    final AtomicInteger count = this.count;
    final ReentrantLock takeLock = this.takeLock;
    //可中断形式获取takeLock
    takeLock.lockInterruptibly();
    try {
        while (count.get() == 0) {
            //队列为空,阻塞
            notEmpty.await();
        }
        //从队列中移除尾结点
        x = dequeue();
        //当前数量-1
        c = count.getAndDecrement();
        if (c > 1)
            //唤醒一个出队线程
            notEmpty.signal();
    } finally {
        //释放锁
        takeLock.unlock();
    }
    if (c == capacity)
        //队列由满变为未满,唤醒一个入队线程
        signalNotFull();
    return x;
}

流程和put基本一致,不过获取的可重入锁是takeLock,阻塞的notEmpty

获取元素

java 复制代码
public E peek() {
    if (count.get() == 0)
        return null;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lock();
    try {
        Node<E> first = head.next;
        if (first == null)
            return null;
        else
            return first.item;
    } finally {
        takeLock.unlock();
    }
}

public E element() {
    E x = peek();
    if (x != null)
        return x;
    else
        throw new NoSuchElementException();
}

获取元素两个方法也很简单,就不用多说了。

ArrayBlockingQueue

  1. 属性
java 复制代码
/** The queued items */
final Object[] items;

/** items index for next take, poll, peek or remove */
int takeIndex;

/** items index for next put, offer, or add */
int putIndex;

/** Number of elements in the queue */
int count;


/** Main lock guarding all access */
final ReentrantLock lock;

/** Condition for waiting takes */
private final Condition notEmpty;

/** Condition for waiting puts */
private final Condition notFull;

ArrayBlockingQueue基于数组 实现,相比于LinkedBlockingQueue只使用了一个锁lock同时控制所有操作,所以同时插入和移除操作并发时,效率是不及LinkedBlockingQueue的。

  1. 构造方法
java 复制代码
//默认非公平策略
public ArrayBlockingQueue(int capacity) {
    this(capacity, false);
}
//可设置公平策略
public ArrayBlockingQueue(int capacity, boolean fair) {
    if (capacity <= 0)
        throw new IllegalArgumentException();
    this.items = new Object[capacity];
    lock = new ReentrantLock(fair);
    notEmpty = lock.newCondition();
    notFull =  lock.newCondition();
}

ArrayBlockingQueue支持一个可选的公平策略,用于排序等待的生产者和消费者线程。默认情况下,不保证这种排序。然而,公平性设置为true的队列以FIFO顺序授予线程访问权限。公平性通常会降低吞吐量,但会减少可变性并避免饥饿。

  1. 插入元素
java 复制代码
public void put(E e) throws InterruptedException {
    checkNotNull(e);
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == items.length)
            notFull.await();
        enqueue(e);
    } finally {
        lock.unlock();
    }
}

//插入数组
private void enqueue(E x) {
    // assert lock.getHoldCount() == 1;
    // assert items[putIndex] == null;
    final Object[] items = this.items;
    items[putIndex] = x;
    if (++putIndex == items.length)
        //到队尾,从头继续
        putIndex = 0;
    count++;
    notEmpty.signal();
}

和LinkedBlockingQueue插入元素过程基本类似,不过是插入到数组中。移除和获取元素差异不大就不展开细说。

总结

BlockingQueue提供了插入移除 操作的阻塞方法,这些操作实现较简单,核心是ReentrantLock提供锁和Condition阻塞和唤醒线程。要真正理解阻塞队列的实现,更需要的理解基于AQS实现的可重入锁以及volatile可见性实现原理,后续单独写一篇讲解。

参考资料

相关推荐
字节跳动数据库11 分钟前
文章分享——相似函数处理方法
人工智能·后端·程序员
云技纵横11 分钟前
@Transactional 失效的 7 种场景:第 5 种最难排查
后端
用户67570498850229 分钟前
你知道 Go 结构体和结构体指针调用的区别吗?一文带你彻底搞懂!
后端·go
程序员cxuan1 小时前
读懂 Claude Code 架构分析系列,第一篇,开始!
人工智能·后端·架构
用户6757049885021 小时前
面试官问“装饰器模式”,这样回答薪资多要 3000!
后端
tntxia1 小时前
Geo Scene域名修改引起的一些问题
后端
用户298698530141 小时前
Java 实现 Word 文档加密与权限解除
java·后端
vanuan1 小时前
给你的A2A-Agent加把锁-认证鉴权实战指南
后端
Yeats_Liao1 小时前
14:Servlet中的页面跳转-Java Web
java·后端·架构