Java并发编程之LinkedBlockingQueue阻塞队列详解

简介

LinkedBlockingQueue是一个用链表实现的有界阻塞队列,此队列按照先进先出(FIFO)的原则对元素进行排序。

此队列的默认和最大长度是Integer.MAX_VALUE,LinkedBlockingQueue类有三个构造方法:

// 默认构造方法,该方法会调用this(Integer.MAX_VALUE),即默认最大长度是Integer.MAX_VALUE
public LinkedBlockingQueue();
// 参数capacity为指定的队列最大长度
public LinkedBlockingQueue(int capacity);
// 根据Collection来创建队列,会将集合中的元素加入到队列中
public LinkedBlockingQueue(Collection<? extends E> c);

LinkedBlockingQueue源码详解

LinkedBlockingQueue类定义为:

public class LinkedBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable

该类同样继承了AbstractQueue抽象类并实现了BlockingQueue接口,这里不再叙述。

LinkedBlockingQueue类中的数据都被封装成了Node对象:

static class Node<E> {
    // 节点数据
    E item;

    // 下一节点
    Node<E> next;

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

同时,LinkedBlockingQueue类通过ReentrantLock和Condition来确保多线程环境下的同步问题。

/** The capacity bound, or Integer.MAX_VALUE if none */
private final int capacity;

/** Current number of elements */
private final AtomicInteger count = new AtomicInteger();

/**
* Head of linked list.
* Invariant: head.item == null
*/
transient Node<E> head;

/**
* Tail of linked list.
* Invariant: last.next == null
*/
private transient Node<E> last;

/** Lock held by take, poll, etc */
private final ReentrantLock takeLock = new ReentrantLock();

/** Wait queue for waiting takes */
private final Condition notEmpty = takeLock.newCondition();

/** Lock held by put, offer, etc */
private final ReentrantLock putLock = new ReentrantLock();

/** Wait queue for waiting puts */
private final Condition notFull = putLock.newCondition();
  • capacity:阻塞队列的容量,默认值为Integer.MAX_VALUE,可通过构造函数设置
  • count:阻塞队列中的元素个数
  • head:阻塞队列头结点
  • last:阻塞队列尾结点
  • takeLock:获取元素时都必须获取该锁
  • notEmpty:出队条件
  • putLock:添加元素时都必须获取该锁
  • notFull:入队条件

LinkedBlockingQueue类有两个独占锁:takeLock和putLock,也就是说,添加和删除元素并不是互斥操作,可以同时进行,这样也就可以提高吞吐量。

入队

我们来看一下add(E e)方法:

LinkedBlockingQueue类的add(E e)方法继承自AbstractQueue:

public boolean add(E e) {
    if (offer(e))
        return true;
    else
        throw new IllegalStateException("Queue full");
}

LinkedBlockingQueue类实现的offer(E e)方法为:

public boolean offer(E e) {
    // 若插入的数据为null,则抛出NullPointerException异常
    if (e == null) throw new NullPointerException();
    // 获取队列中的元素个数
    final AtomicInteger count = this.count;
    // 若队列已满则返回false
    if (count.get() == capacity)
        return false;
    int c = -1;
    // 将对象构建为Node节点
    Node<E> node = new Node<E>(e);
    // 获取putLock独占锁
    final ReentrantLock putLock = this.putLock;
    putLock.lock();
    try {
        // 若队列未满
        if (count.get() < capacity) {
            // 添加元素
            enqueue(node);
            // 更新元素个数值
            c = count.getAndIncrement();
            // 若队列仍然未满,唤醒阻塞在非满条件上的线程
            if (c + 1 < capacity)
                notFull.signal();
        }
    } finally {
        // 释放putLock独占锁
        putLock.unlock();
    }
    // 队列中还有一条数据(c的初始值为-1),唤醒阻塞在非空条件上的线程
    if (c == 0)
        signalNotEmpty();
    return c >= 0;
}

LinkedBlockingQueue类的offer(E e)方法与ArrayBlockingQueue类相比,主要有两处不同:

1、ArrayBlockingQueue类在添加元素之后,会唤醒消费线程,而LinkedBlockingQueue类在添加完元素之后,若队列未满,它会唤醒其他的生产进程,产生该差异的主要原因是ArrayBlockingQueue使用了一个独占锁,而LinkedBlockingQueue使用了两个独占锁,这两个锁可以分别控制元素入队与出队操作,而不会产生同步问题,可以提高吞吐量。

2、LinkedBlockingQueue类在添加完元素之后,也可能会唤醒消费线程,唤醒的前提是c==0,那为什么是这样呢?这也与两个独占锁有关,因为消费线程在消费数据时,只会获取takeLock,所以说,生产线程生产数据时,不会影响消费线程,若队列中的元素一直>0,消费线程是不会停止的,可能没有消费线程停止 。而在c=0的时,这表明,可能有消费线程已经停止,这时,就需要唤醒消费线程,来取出队列中的数据。

出队

我们这边来看一下poll()方法:

public E poll() {
    // 获取队列的元素个数
    final AtomicInteger count = this.count;
    // 队列为空,则直接返回null
    if (count.get() == 0)
        return null;
    E x = null;
    int c = -1;
    // 获取takeLock独占锁
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lock();
    try {
        // 若队列非空
        if (count.get() > 0) {
            // 元素出队
            x = dequeue();
            // 更新元素个数值
            c = count.getAndDecrement();
            // 如果队列非空,则唤醒阻塞在非空条件上的线程
            if (c > 1)
                notEmpty.signal();
        }
    } finally {
        // 释放takeLock独占锁
        takeLock.unlock();
    }
    // 若队列已满,则唤醒阻塞在非满条件上的线程
    if (c == capacity)
        signalNotFull();
    return x;
}

该类的poll()方法与ArrayBlockingQueue类相比,仍然有两处不同,具体原因不再解释,类似于offer(E e)方法。

因为LinkedBlockingQueue类具有两个独占锁,该类的其他方法与ArrayBlockingQueue相比,也有一些不同,比如remove(Object o)方法:

public boolean remove(Object o) {
    // 若要删除的元素为null,则直接返回false
    if (o == null) return false;
    // 获取两个独占锁
    fullyLock();
    try {
        // 遍历查找要删除的节点,将其删除,成功返回true,否则,返回false
        for (Node<E> trail = head, p = trail.next;
                p != null;
                trail = p, p = p.next) {
            if (o.equals(p.item)) {
                unlink(p, trail);
                return true;
            }
        }
        return false;
    } finally {
        // 释放两个独占锁
        fullyUnlock();
    }
}

我们看到该方法调用了一个fullyLock()方法

void fullyLock() {
    putLock.lock();
    takeLock.lock();
}

该方法是获取两个独占锁,为什么要或者这两个锁呢?因为remove(Object o)方法删除的元素的位置不确定,可能会产生同步问题,所以要获取两个锁。

相关博客

Java并发编程之ArrayBlockingQueue阻塞队列详解

Java并发编程之ReentrantLock详解

 Java并发编程之Condition详解

参考资料

方腾飞:《Java并发编程的艺术》

猜你喜欢

转载自blog.csdn.net/qq_38293564/article/details/80583831