LinkedBlockingQueue源码分析

215 阅读5分钟

概述

LinkedBlockingQueue用链表实现的有界阻塞队列,初始化时如果没有传入容量,则容量时Intger.MAX_VALUE,是FIFO队列,因为入队和出队方法各是一把锁,所以一般情况下并发性能优于ArrayBlockingQueue。本篇一起来看源码,LinkedBlockingQueue的并发控制是用ReentrantLock加Condition。看本篇前可以先看我写的另外三篇文章,ReentrantLock源码分析, Condition源码分析,阻塞队列简介。

类结构

public class LinkedBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {
    //节点数据结构,可以看到是单向列表
    static class Node<E> {
        E item;
        //指向下一节点
        Node<E> next;
        Node(E x) { item = x; }
    }

    //容量,即队列的最大元素数,说明该队列是有界的
    private final int capacity;

    //当前队列的元素数量,用AtomicInteger保证并发的原子性
    private final AtomicInteger count = new AtomicInteger();

    //队列的头节点,出队从这里
    transient Node<E> head;

    //队列的尾节点,入队从这里,可以看到,队列是FIFO的
    private transient Node<E> last;

    //出队的锁控制
    private final ReentrantLock takeLock = new ReentrantLock();

    //队列为空时,出队take方法的线程进入等待
    private final Condition notEmpty = takeLock.newCondition();

    //入队的锁控制
    private final ReentrantLock putLock = new ReentrantLock();

    //入队时队列已满,将入队线程进入等待
    private final Condition notFull = putLock.newCondition();
    。。。
}

上面给出了LinkedBlockingQueue主体结构,每个节点存放目标元素并指向下一节点,并发的控制使用ReentrantLock,需要等待用Condition。在阻塞队列简介中,我们说过入队方法等待,只有put和带超时时间的offer方法。所以我们后面可以看到只有这两个方法会用到notFull.await。
另外我们学习ReentrantLock时知道,它里面也有队列(它的队列节点是线程,获取锁未成功的线程进入队列等候,调用condition.await,就进入condition队列),请不要和这里的混淆,这里是目标元素的队列。
构造方法

public LinkedBlockingQueue(int capacity) {
    if (capacity <= 0) throw new IllegalArgumentException();
    //初始化容量
    this.capacity = capacity;
    //初始化头尾的指针,可以看到队列的头尾指针不可能为null
    last = head = new Node<E>(null);
}

put方法

public void put(E e) throws InterruptedException {
    //不允许空元素
    if (e == null) throw new NullPointerException();
    // 队列的节点数量,初始化负数,如果后面还是负数表示入队失败offer方法会用到
    int c = -1;
    Node<E> node = new Node<E>(e);
    final ReentrantLock putLock = this.putLock;
    final AtomicInteger count = this.count;
    //入队前先加锁,将后面的并发操作变成单线程操作
    putLock.lockInterruptibly();
    try {
        //队列已满时,线程进入等待队列
        //被唤醒后会再次判断,是否已满,所以进行while循环
        while (count.get() == capacity) {
            notFull.await();
        }
        //入队,到了这里肯定是线程没满,且只有一个线程操作,所以直接入队
        enqueue(node);
        //拿到入队前的队列节点数量,并原子性加1,这里为什么要使用AtomicIntege呢?
        //虽然这里只会有一个线程运行,但还有出队takeLock,使用另一把锁,也在对count操作,产生竞争
        c = count.getAndIncrement();
        //如果队列没满唤起notFull等待的线程
        if (c + 1 < capacity)
            notFull.signal();
    } finally {
        putLock.unlock();
    }
    //如果队列是空,唤起notEmpty等待的线程
    if (c == 0)
        signalNotEmpty();
}

enqueue方法

private void enqueue(Node<E> node) {
    // 前面代码上了锁,这里是单线程操作,可以看到代码很简单,就是入队尾
    last = last.next = node;
}

signalNotEmpty方法

//唤醒notEmpty的Condition条件等待
//这个等待是出队方法take或超时poll,出队时,队列为空,线程进行的等待
private void signalNotEmpty() {
    //为什么这里加锁呢?
    //因为每个出队方法(take、poll)出队后,队列不为空都会进行唤醒,后面代码会看到
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lock();
    try {
        notEmpty.signal();
    } finally {
        takeLock.unlock();
    }
}

put方法就介绍完了,offer方法很类似,我们简单看下
offer方法

public boolean offer(E e) {
    if (e == null) throw new NullPointerException();
    final AtomicInteger count = this.count;
    //队列满时直接返回
    if (count.get() == capacity)
        return false;
    int c = -1;
    Node<E> node = new Node<E>(e);
    final ReentrantLock putLock = this.putLock;
    putLock.lock();
    try {
        //队列没满进行入队,满了直接跳过if
        if (count.get() < capacity) {
            //入队,上面已经分析
            enqueue(node);
            c = count.getAndIncrement();
            //队列没满,唤醒入队的notFull条件等待线程
            //为什么是c+1?因为使用的是count.getAndIncrement返回的值是加1前的值,即入队前的节点个数
            //所以c+1才是当前队列的节点数
            if (c + 1 < capacity)
                notFull.signal();
        }
    } finally {
        putLock.unlock();
    }
    //队列为空,进行加锁唤醒notEmpty条件等待线程
    if (c == 0)
        signalNotEmpty();
    return c >= 0;
}

take 方法

public E take() throws InterruptedException {
    E x;
    int c = -1;
    final AtomicInteger count = this.count;
    //加take读写锁
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lockInterruptibly();
    try {
        //当count为0时,当前线程进行条件等待
        while (count.get() == 0) {
            notEmpty.await();
        }
        //被唤醒后,队列不为空,进行出队操作
        x = dequeue();
        //count值原子性减1
        c = count.getAndDecrement();
        //如果队列不为空,对notEmpty条件等待的线程唤醒,所以put方法的notEmpty.signal要先加takeLock锁
        if (c > 1)
            notEmpty.signal();
    } finally {
        takeLock.unlock();
    }
    //take前队列时满的,可能有线程入队进行了等待,所以唤醒notFull条件等待线程
    if (c == capacity)
        signalNotFull();
    return x;
}
private E dequeue() {
    //我们从上面的构造方法中看到,初始化时队列的头尾指针都指向了一个item为null的空节点
    //所以这里出队就是把head.next节点的item返回,把头节点后移一个就行了
    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;
}

唤醒notFull条件等待线程

private void signalNotFull() {
    final ReentrantLock putLock = this.putLock;
    putLock.lock();
    try {
        notFull.signal();
    } finally {
        putLock.unlock();
    }
}

take方法就分析完啦,poll方法和take方法类似,比较简单就不分析了,我们来看下remove方法。
remove方法

public boolean remove(Object o) {
    if (o == null) return false;
    //将putLock,takeLock全部锁定,为什么?
    //因为remove object可能操作的是头节点,也可能操作的是尾节点,都加锁才能保证头尾节点的操作不会因竞争产生错误。
    fullyLock();
    try {
        for (Node<E> trail = head, p = trail.next;
             p != null;
             trail = p, p = p.next) {
             //如果节点中的对象和remove的目标对象相等,进行移除
            if (o.equals(p.item)) {
                //移除节点
                unlink(p, trail);
                return true;
            }
        }
        //没找到节点返回false
        return false;
    } finally {
        fullyUnlock();
    }
}

fullyLock方法

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

unlink 方法

void unlink(Node<E> p, Node<E> trail) {
    // p是待移除节点,trail是p的前继节点
    p.item = null;
    //将p的后继节点赋给trail的next
    //p的item设为null,完成了remove操作
    trail.next = p.next;
    //如果p是尾节点,需要将trail赋值给last引用
    if (last == p)
        last = trail;
    //如果移除之前队列是满的,可能有入队线程在等待入队,进行唤醒操作    
    if (count.getAndDecrement() == capacity)
        notFull.signal();
}