文章目录

  • 1. LinkedBlockingQueue 简介
  • 2. LinkedBlockingQueue 的关键属性
  • 3. LinkedBlockingQueue 的元素存取流程
  • 3.1 添加元素
  • 3.2 取出元素


1. LinkedBlockingQueue 简介

LinkedBlockingQueue 是线程池默认使用的任务队列,为了满足多线程环境下元素出入队列的安全性,其主要有以下特点:

  1. LinkedBlockingQueue 是基于链表结构的阻塞队列,默认容量为 Integer.MAX_VALUE,可以认为是无界队列
  2. LinkedBlockingQueue 队列按 FIFO(先进先出)排序元素
  3. LinkedBlockingQueue 内部使用两个独占锁来保证线程安全,其中写入锁用于控制添加元素的操作,取出锁用于控制取出元素的操作。新元素从队列尾部入队,取出元素则从队列头部移除,因此可以同时在队列头部和队列尾部并发地进行元素取出、添加操作

2. LinkedBlockingQueue 的关键属性

LinkedBlockingQueue 中添加到队列的数据都将被封装成 Node 节点,可以看到 Node 的结构比较简单,只有一个后指针指向下一个节点,形成单向链表结构

static class Node<E> {
        E item;
        
        Node<E> next;

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

LinkedBlockingQueue 中关键属性如下,其中 headlast 分别指向队列的头节点和尾节点。LinkedBlockingQueue 内部分别使用 takeLockputLock 两个独占锁对并发进行控制,在高并发的情况下生产者和消费者可以并行地操作队列中的数据,也就大大提高了队列的并发性能

// 最大容量上限,默认是 Integer.MAX_VALUE
private final int capacity;

// 元素计数器,这是个原子类。因为读写分别使用不同的锁,但都会访问这个属性,所以它需要保证线程安全
private final AtomicInteger count = new AtomicInteger();

// 队列头结点
private transient Node<E> head;

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

// 队头出队锁
private final ReentrantLock takeLock = new ReentrantLock();

// 等待获取出队锁的线程的等待队列
private final Condition notEmpty = takeLock.newCondition();

// 队尾入队锁
private final ReentrantLock putLock = new ReentrantLock();

// 等待获取入队锁的线程的等待队列
private final Condition notFull = putLock.newCondition();

3. LinkedBlockingQueue 的元素存取流程

3.1 添加元素

java线程池默认阻塞队列 线程池默认的队列_队列

LinkedBlockingQueue 添加元素的主要方法是 LinkedBlockingQueue#put()/LinkedBlockingQueue#offer(),本文以 LinkedBlockingQueue#put() 为例

  1. LinkedBlockingQueue#put() 方法的处理步骤如下:
  1. 当前线程首先获取 putLock 写入锁
  2. 获取锁成功,则判断当前队列是否已经满了,队列容量已满的情况下不允许添加新的元素,此时当前线程需要挂起等待,notFull.await() 类似于 Object.wait()方法
  3. 线程唤醒后依然在 while 循环中判断队列容量是否已满,因为消费线程会不断取出队列中的元素,故此处循环正常情况下是一定能够跳出的。跳出后调用 LinkedBlockingQueue#enqueue() 方法将元素入队
  4. 当前线程完成添加元素的操作,检查队列容量是否已满,如果队列没有达到容量上限显然可以继续添加元素,则将在等待 putLock 写入锁的一个线程唤醒,让它能完成添加元素的操作,notFull.signal() 类似于 Object.notify()方法,是 AQS 的一种实现机制,
  5. 当前线程释放 putLock 写入锁
  6. 如果队列在本次添加元素之前是空的,那么很有可能存在消费线程挂起等待可消费的元素,故此时调用 LinkedBlockingQueue#signalNotEmpty() 方法唤醒一个在等待的消费线程

LinkedBlockingQueue#put() 方法会一直阻塞直到能够把元素添加到队列中,也就是除非发生异常,否则元素是必然能添加到队列中的
LinkedBlockingQueue#offer() 方法则允许添加元素失败,无法将元素添加到队列时直接返回 false 即可结束调用

public void put(E e) throws InterruptedException {
     if (e == null) throw new NullPointerException();
     // Note: convention in all put/take/etc is to preset local var
     // holding count negative to indicate failure unless set.
     int c = -1;
     Node<E> node = new Node<E>(e);
     final ReentrantLock putLock = this.putLock;
     final AtomicInteger count = this.count;
     putLock.lockInterruptibly();
     try {
         /*
          * Note that count is used in wait guard even though it is
          * not protected by lock. This works because count can
          * only decrease at this point (all other puts are shut
          * out by lock), and we (or some other waiting put) are
          * signalled if it ever changes from capacity. Similarly
          * for all other uses of count in other wait guards.
          */
         while (count.get() == capacity) {
             notFull.await();
         }
         enqueue(node);
         c = count.getAndIncrement();
         if (c + 1 < capacity)
             notFull.signal();
     } finally {
         putLock.unlock();
     }
     if (c == 0)
         signalNotEmpty();
 }
  1. LinkedBlockingQueue#enqueue() 方法的处理如下,非常简单易懂,就是元素从队列尾部入队
private void enqueue(Node<E> node) {
     // assert putLock.isHeldByCurrentThread();
     // assert last.next == null;
     last = last.next = node;
 }

3.2 取出元素

LinkedBlockingQueue 取出元素的主要方法为 LinkedBlockingQueue#take()/LinkedBlockingQueue#poll(),本文以 LinkedBlockingQueue#take() 为例

  1. LinkedBlockingQueue#take() 方法的处理步骤如下:
  1. 当前线程首先获取 takeLock 取出锁
  2. 获取锁成功,则判断当前队列是否是空的,队列元素数量为 0 的情况下无法取出元素,此时当前线程需要挂起等待,调用 notEmpty.await() 方法
  3. 线程唤醒后依然在 while 循环中判断队列是否为空,因为生产线程会往队列中添加元素,故此处循环是一定能够跳出的。跳出后调用 LinkedBlockingQueue#dequeue() 方法将队列头部元素出队
  4. 元素出队后,检查队列是否是空的,如果队列不为空显然可以继续取出元素,则将在等待 takeLock 取出锁的一个线程唤醒,让它能完成取出元素的操作,调用notEmpty.signal()方法
  5. 当前线程释放 takeLock 取出锁
  6. 如果队列在本次取出元素之前是满的,那么很有可能存在生产线程挂起等待时机添加元素,故此时调用 LinkedBlockingQueue#signalNotFull() 方法唤醒一个在等待的生产线程

LinkedBlockingQueue#take() 方法会一直阻塞直到能够从队列中取到元素,也就是除非发生异常,否则该方法是必然能获取到元素的
LinkedBlockingQueue#poll() 方法则允许获取元素失败,无法从队列中获取元素时直接返回 null 即可结束调用

public E take() throws InterruptedException {
     E x;
     int c = -1;
     final AtomicInteger count = this.count;
     final ReentrantLock takeLock = this.takeLock;
     takeLock.lockInterruptibly();
     try {
         while (count.get() == 0) {
             notEmpty.await();
         }
         x = dequeue();
         c = count.getAndDecrement();
         if (c > 1)
             notEmpty.signal();
     } finally {
         takeLock.unlock();
     }
     if (c == capacity)
         signalNotFull();
     return x;
 }
  1. LinkedBlockingQueue#dequeue() 方法其实就是将元素从队列头部移除,使其出队
private E dequeue() {
     // assert takeLock.isHeldByCurrentThread();
     // assert head.item == null;
     Node<E> h = head;
     Node<E> first = h.next;
     h.next = h; // help GC
     head = first;
     E x = first.item;
     first.item = null;
     return x;
 }