JUC集合类 LinkedBlockingQueue源码解析 JDK8

前言

LinkedBlockingQueue是一种FIFO(first-in-first-out 先入先出)的有界阻塞队列,底层是单链表,也支持从内部删除元素。并发操作依赖于加锁的控制,支持阻塞式的入队出队操作。

相比ArrayBlockingQueue的一个Lock,LinkedBlockingQueue使用了两个Lock,分别对应入队动作和出队动作,这便提高了并发量。

JUC框架 系列文章目录

成员

    static class Node<E> {
        E item;

        Node<E> next;

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

LinkedBlockingQueue的底层实现基于单链表,上面是单链表的Node定义。

    /** 容量,毕竟这是一个有界队列 */
    private final int capacity;

    /** 大小,元素的个数 */
    private final AtomicInteger count = new AtomicInteger();

    //队首指针
    transient Node<E> head;

    //队尾指针
    private transient Node<E> last;

上面就是些基于单链表的队列的必备成员。之所以需要使用AtomicInteger,是因为有两个线程(入队线程、出队线程)可能同时在修改它,所以用原子类来保持count的正确性。

    /** 出队线程需要竞争这把锁,竞争到了才能出队,也就是说同时只有一个线程能出队 */
    private final ReentrantLock takeLock = new ReentrantLock();

    /** 出队线程可能会暂时阻塞在这个AQS条件队列里,当发现队列已空时 */
    private final Condition notEmpty = takeLock.newCondition();

    /** 入队线程需要竞争这把锁,竞争到了才能入队,也就是说同时只有一个线程能入队 */
    private final ReentrantLock putLock = new ReentrantLock();

    /** 入队线程可能会暂时阻塞在这个AQS条件队列里,当发现队列已满时 */
    private final Condition notFull = putLock.newCondition();

为了保证last的正确性,只有竞争到putLock的入队线程才能执行入队动作。这样就只有一个线程在修改last。
在这里插入图片描述

上图展示了入队线程的通用流程。当入队线程从notFull.await()处恢复执行时,已经又重新获得了putLock,然后入队线程即将执行入队动作,别的线程也不可能和它竞争入队了。

为了保证head的正确性,只有竞争到takeLock的出队线程才能执行出队动作。这样就只有一个线程在修改head。
在这里插入图片描述
上图展示了出队线程的通用流程。当出队线程从notEmpty.await()处恢复执行时,已经又重新获得了takeLock,然后出队线程即将执行出队动作,别的线程也不可能和它竞争出队了。

构造器

    public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);//默认大小
    }

    public LinkedBlockingQueue(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;
        last = head = new Node<E>(null);//队列始终有一个dummy node作为head
    }

默认大小为Integer.MAX_VALUE,当然这也是最大大小。队列始终有一个dummy node作为head。

入队

add

//AbstractQueue.java
    public boolean add(E e) {
        if (offer(e))
            return true;
        else//返回false的处理不一样
            throw new IllegalStateException("Queue full");
    }
    
//Queue.java(接口文件)
	boolean offer(E e);

这个方法在LinkedBlockingQueue.java中找不到,因为你直接调用的是父类实现。add依靠于子类的offer实现。所以,add就是在调用自己的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 {
            //入队前获取最新count,来判断
            if (count.get() < capacity) {//只进行一次尝试
                enqueue(node);
                c = count.getAndIncrement();//执行count++
                if (c + 1 < capacity)//如果新大小 小于容量
                    notFull.signal();//让一个入队线程离开AQS条件队列
            }
        } finally {
            putLock.unlock();
        }
        if (c == 0)//如果新大小为1
            signalNotEmpty();//让一个出队线程离开AQS条件队列
        return c >= 0;//如果新大小>=1,说明入队成功
    }

该函数只进行一次尝试,如果队列当前已满,就直接退出;如果队列当前非满,才执行入队动作。它甚至会在获得锁之前,就判断队列满的情况。

if (c + 1 < capacity)if (c == 0)两处用的比较运算符不一样:

  • if (c + 1 < capacity)处。因为当前线程正持有putLock中(所以count不可能被别的线程增加),但count可能由于别的出队线程而减小,所以只要新size小于capacity,就唤醒后面的入队线程。
  • if (c == 0)处。当旧size为0时,才去唤醒后面的出队线程。因为旧size为正数的话,出队线程是不会阻塞的,所以只需要精确判断这种情况。

put

  1. putLock.lockInterruptibly()中:因获得不到锁而阻塞,是可以被中断而抛出中断异常。
  2. try代码块中:如果没有中断来临,该函数会一直阻塞直到它成功入队。如果队列一直是满的,我们可以通过中断线程来终止put的调用。
    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.lockInterruptibly();//可能抛出中断异常
        try {
            while (count.get() == capacity) {//从下一句恢复后,需要检查是否为虚假唤醒
                notFull.await();//可能抛出中断异常
            }
            //执行到这里,count肯定只会比capacity小
            enqueue(node);
            c = count.getAndIncrement();//执行count++
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            putLock.unlock();
        }
        if (c == 0)
            signalNotEmpty();
    }

这里需要讲一下count的正确性,从while (count.get() == capacity)退出时count肯定小于capacity了,并且我们不用担心接下来的并发问题:

  • 不用担心另一种线程——出队线程。因为出队线程只会使得capacity变小,所以即使有出队线程的并发,count还是小于capacity的。
  • 不用担心自己线程——入队线程。因为当前线程还拥有着putLock。
  • 综上,从循环退出后,count将保持小于capacity。而这是执行enqueue(node)的前提。
    private void enqueue(Node<E> node) {
        // assert putLock.isHeldByCurrentThread();
        // assert last.next == null;
        last = last.next = node;
    }

enqueue函数将新节点插到尾部,然后last更新为新节点。

超时offer

  1. putLock.lockInterruptibly()中:因获得不到锁而阻塞,是可以被中断而抛出中断异常。
  2. try代码块中:如果没有中断或超时来临,该函数会一直阻塞直到它成功入队。超时前我们可以通过中断线程来终止offer的调用,超时后如果队列还是满的offer将退出。
    public boolean offer(E e, long timeout, TimeUnit unit)
        throws InterruptedException {

        if (e == null) throw new NullPointerException();
        long nanos = unit.toNanos(timeout);
        int c = -1;
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
        putLock.lockInterruptibly();//可能抛出中断异常
        try {
            while (count.get() == capacity) {
                if (nanos <= 0)//如果队列是满的,且剩余等待时间<= 0这代表不用等待,所以直接返回false
                    return false;
                //下面这句可能抛出中断异常
                nanos = notFull.awaitNanos(nanos);//返回剩余等待时间,如果超时,返回值小于0
            }
            enqueue(new Node<E>(e));
            c = count.getAndIncrement();
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            putLock.unlock();
        }
        if (c == 0)
            signalNotEmpty();
        return true;
    }

入队方法总结

入队方法是否等待队列满时的处理返回值返回值含义抛出中断异常的含义
add一次入队尝试,从不等待抛出"Queue full"异常true
-
入队成功
-
-
offer一次入队尝试,从不等待返回falsetrue
false
入队成功
入队失败
-
put入队尝试失败后,会等待进入条件队列继续等待void只要从put调用处正常返回,就代表入队成功signal来临前,中断发生
超时offer入队尝试失败后,会等待如果没超时,则进入条件队列继续等待;
如果超时了,返回false
true
false
规定时间内,入队成功
规定时间内,没有入队
signal或超时来临前,中断发生

出队

remove

//AbstractQueue.java
    public E remove() {
        E x = poll();
        if (x != null)
            return x;
        else
            throw new NoSuchElementException();
    }

    
//Queue.java(接口文件)
    E poll();

同样的,remove就是在调用自己的poll方法。

poll

    public E poll() {
        final AtomicInteger count = this.count;
        if (count.get() == 0)//提前进行一个快捷判断,发现队列已空,则退出
            return null;//返回null,代表出队失败
        E x = null;
        int c = -1;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
            //出队前获取最新count,来判断
            if (count.get() > 0) {//只进行一次尝试
                x = dequeue();
                c = count.getAndDecrement();
                if (c > 1)//如果旧大小 大于等于2
                    notEmpty.signal();//让一个出队线程离开AQS条件队列
            }
        } finally {
            takeLock.unlock();
        }
        if (c == capacity)//如果旧大小 等于 容量
            signalNotFull();//才需要让一个入队线程离开AQS条件队列
        return x;//返回非null
    }

该函数只进行一次尝试,如果队列当前已空,就直接退出;如果队列当前非空,才执行出队动作。它甚至会在获得锁之前,就判断队列空的情况。

    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;
    }

dequeue函数很简单:

  • 让head后移一个节点。
  • 让移除掉的旧head的next指针指向自己,以区别于队尾节点(队尾节点的next为null)。
  • 让新head变成dummy node之前,保存其item以返回。

take

  1. takeLock.lockInterruptibly()中:因获得不到锁而阻塞,是可以被中断而抛出中断异常。
  2. try代码块中:如果没有中断来临,该函数会一直阻塞直到它成功出队。如果队列一直是空的,我们可以通过中断线程来终止take的调用。
    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();//可能抛出中断异常
            }
            //执行到这里,count肯定只会比0大
            x = dequeue();
            c = count.getAndDecrement();
            if (c > 1)
                notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
        if (c == capacity)
            signalNotFull();
        return x;
    }

超时poll

  1. takeLock.lockInterruptibly()中:因获得不到锁而阻塞,是可以被中断而抛出中断异常。
  2. try代码块中:如果没有中断或超时来临,该函数会一直阻塞直到它成功出队。超时前我们可以通过中断线程来终止poll的调用,超时后如果队列还是空的poll将退出。
    public E poll(long timeout, TimeUnit unit) throws InterruptedException {
        E x = null;
        int c = -1;
        long nanos = unit.toNanos(timeout);
        final AtomicInteger count = this.count;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lockInterruptibly();//可能抛出中断异常
        try {
            while (count.get() == 0) {
                if (nanos <= 0)//如果队列是空的,且剩余等待时间<= 0这代表不用等待,所以直接返回null
                    return null;
                //下面这句可能抛出中断异常
                nanos = notEmpty.awaitNanos(nanos);//返回剩余等待时间,如果超时,返回值小于0
            }
            x = dequeue();
            c = count.getAndDecrement();
            if (c > 1)
                notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
        if (c == capacity)
            signalNotFull();
        return x;
    }

出队方法总结

出队方法是否等待队列空时的处理返回值返回值含义抛出中断异常的含义
remove一次出队尝试,从不等待抛出NoSuchElementException非null
-
队列非空
-
-
poll一次出队尝试,从不等待返回null非null
null
队列非空
队列空
-
take出队尝试失败后,会等待进入条件队列继续等待void只要从take调用处返回,就代表出队成功signal来临前,中断发生
超时poll出队尝试失败后,会等待如果没超时,则进入条件队列继续等待;
如果超时了,返回false
非null
null
规定时间内,出队成功
规定时间内,没有出队
signal或超时来临前,中断发生

内部删除 remove(Object o)

    public boolean remove(Object o) {
        if (o == null) return false;
        fullyLock();
        try {
            //p是循环变量,trail用来保存旧p
            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();
        }
    }

循环遍历找那个元素,如果找到了就删除,这可能是一个内部删除。总之,有可能是队首或队尾,所以需要两个锁都先持有了再做操作,结束后两把锁都释放了。

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

void fullyUnlock() {
    takeLock.unlock();
    putLock.unlock();
}
    void unlink(Node<E> p, Node<E> trail) {
        // assert isFullyLocked();
        p.item = null;//逻辑删除
        trail.next = p.next;//将p从链表中移除
        if (last == p)//这种情况需要更新last
            last = trail;
        if (count.getAndDecrement() == capacity)
            notFull.signal();
    }

一个正常的从单链表中删除一个节点的操作。但注意没有将p的next指针指向自己,因为这样可以让迭代器继续从逻辑删除的p节点后继续遍历。

注意,此unlink函数不需要去执行if (c > 1) notEmpty.signal()的操作,因为能执行这个函数说明队列中至少有一个元素,那么在fullyLock之前就不可能有出队线程因为队列为空而阻塞。

获取操作

peek

    public E peek() {
        if (count.get() == 0)//快捷判断,如果队列空则直接返回null
            return null;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();//先拿到出队锁,以免有人更新head
        try {
            Node<E> first = head.next;
            if (first == null)
                return null;
            else
                return first.item;
        } finally {
            takeLock.unlock();
        }
    }

element

//AbstractQueue.java
    public E element() {
        E x = peek();
        if (x != null)
            return x;
        else//队列为空,抛出异常
            throw new NoSuchElementException();
    }

迭代器

此迭代器是弱一致性的。因为即使节点被删除,迭代器也会照样返回被删除节点的item。

弱一致性是因为并发操作。当迭代器遍历到某个位置后,你调用hasNext返回true说明下一个节点存在。但之后有别人删除掉了你的这个节点,然后你再调用next()理论上来说我应该返回这个节点的item给你,但删除操作会使得节点的item为null,所以迭代器中必须使用E currentElement提前保存。

    private class Itr implements Iterator<E> {
        private Node<E> current;//下一次next()将返回的数据
        private Node<E> lastRet;//上一次next()已返回的数据
        private E currentElement;//下一次next()将返回的数据

        Itr() {
            fullyLock();
            try {
                current = head.next;//构造时,就准备好current的两个成员
                if (current != null)
                    currentElement = current.item;
            } finally {
                fullyUnlock();
            }
        }

        public boolean hasNext() {
            return current != null;//只要current不为null,即使它的item变成null了,
            //接下来的next()我们也得返回一个非null值,所以需要用currentElement提前保存
        }

        private Node<E> nextNode(Node<E> p) {
            for (;;) {
                Node<E> s = p.next;
                if (s == p)//如果是已出队节点,则跳转到head后继
                    return head.next;
                //1. 如果后继s为null。说明p已经是最后一个节点,遍历已到终点,返回null
                //2. 如果后继s的item不为null。正常节点,返回它即可
                if (s == null || s.item != null)
                    return s;
                //执行到这里,说明后继s的item为null,这是一个逻辑删除的节点,
                //但通过它的后继我们能找到正常节点,所以让p后移,继续循环
                p = s;
            }
        }

        public E next() {
            fullyLock();
            try {
                if (current == null)
                    throw new NoSuchElementException();
                //即将返回这个数据
                E x = currentElement;
                lastRet = current;
                current = nextNode(current);
                currentElement = (current == null) ? null : current.item;
                return x;
            } finally {
                fullyUnlock();
            }
        }

        public void remove() {
            if (lastRet == null)
                throw new IllegalStateException();
            fullyLock();
            try {
                Node<E> node = lastRet;
                lastRet = null;//让此函数无法连续调两次
                for (Node<E> trail = head, p = trail.next;//从头遍历,以找到这个节点
                     p != null;
                     trail = p, p = p.next) {
                    if (p == node) {//找到了同一个对象,才删除它(不是equals判断哦)
                        unlink(p, trail);
                        break;
                    }
                }
            } finally {
                fullyUnlock();
            }
        }
    }

总结

  • 和ConcurrentLinkedQueue一样,初始化时有一个dummy node。也就是说,真正的数据节点,永远是head的后继。
  • 使用了两个Lock,分别负责修改headlast。之所以可以这样,是因为队列非空非满的时候,同时入队出队是互不影响,而且count是一个原子类。
  • 两个Condition的使用,是控制阻塞等待的关键。
  • 两个Lock都是非公平模式的获取锁方式,抢锁更快,提高并发。
  • 出队的节点next指向自身,以区别于队尾节点(next为null)。
©️2020 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页