挖掘队列源码之LinkedBlockingQueue

前言

探索LinkedBlockingQueue是基于JDK1.8,由注释可知是基于单链表的阻塞队列,至于其队列是否有界取决于其队列的容量大小,从严格意义上来说,它是有界队列,按照先进先出的顺序访问,新元素被插入到队列的尾部,从队列头部获取元素,提供在将新元素放入到饱满的队列中会导致阻塞,直到队列出现新的位置才会被唤醒继续往下操作的方法,还支持在从空队列中获取元素会导致阻塞,直到队列出现元素才会被唤醒的方法,通常情况下LinkedBlockingQueue比ArrayBlockingQueue具有更高的吞吐量,因为其内部采用了两个锁,意味着读写分离,所以效率上更快,但这同时也造成了队列的可预测性较差,接下来进入到主题吧。

数据结构


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

        // 队列的容量大小
        private final int capacity;

        // 队列中元素的个数
        private final AtomicInteger count = new AtomicInteger();

        // 队列的头部元素,用于获取元素
        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();
    }


构造函数


    /**
     * 初始化
     * 构建一个Integer.MAX_VALUE容量大小的队列 
     */
    public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }

    /**
     * 初始化
     * @param capacity 容量大小
     */
    public LinkedBlockingQueue(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;
        last = head = new Node<E>(null);
    }

    /**
     * 按顺序插入集合中的元素并初始化
     * @param c 集合
     */
    public LinkedBlockingQueue(Collection<? extends E> c) {
        this(Integer.MAX_VALUE);
        final ReentrantLock putLock = this.putLock;
        putLock.lock();
        try {
            int n = 0;
            for (E e : c) {
                if (e == null)
                    throw new NullPointerException();
                if (n == capacity)
                    throw new IllegalStateException("Queue full");
                enqueue(new Node<E>(e)); // 入队列
                ++n;
            }
            count.set(n);
        } finally {
            putLock.unlock();
        }
    }

简单方法


    /**
     * 唤醒阻塞,从空队列中获取元素导致阻塞直到新元素插入时被唤醒,所以该方法只会被put/offer调用
     */
    private void signalNotEmpty() {
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
            notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
    }

    /**
     * 唤醒阻塞,新元素放入到饱满的队列中导致阻塞直到队列出现空位置时被唤醒
     */
    private void signalNotFull() {
        final ReentrantLock putLock = this.putLock;
        putLock.lock();
        try {
            notFull.signal();
        } finally {
            putLock.unlock();
        }
    }

    /**
     * 入队列
     * 由于是按照先进先出的顺序访问,所以插入的元素放在末尾
     */
    private void enqueue(Node<E> node) {
        last = last.next = node;
    }

    /**
     * 出队列
     * LinkedBlockingQueue在初始化时head = new Node(null),所以在出队列时应该获取其next下一个元素
     * @return 结果值
     */
    private E dequeue() {
        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;
    }

    /**
     * 同时上锁,因为要遍历队列,防止其他线程同时修改其数据结构
     */
    void fullyLock() {
        putLock.lock();
        takeLock.lock();
    }

    /**
     * 释放锁
     */
    void fullyUnlock() {
        takeLock.unlock();
        putLock.unlock();
    }

    /**
     * 插入元素
     * 当队列饱满的情况下会一直阻塞等待,直到被唤醒或被中断
     * @param e 元素 
     */
    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(); // 阻塞等待空位置
            }
            enqueue(node);
            c = count.getAndIncrement();
            /**
             * 通常情况下 notFull应该是在获取元素时去唤醒的,但是这里却出现了,以下是个人的理解
             * 
             * 既然作者考虑了读写分离,那么在读或写的时候就不会出现另一种情况的锁,简单来说,在调用take方法时就不会出现putLock锁,在调用put时就不会出现takeLock锁
             * 但是不行啊,这样子不就无法正常唤醒了吗,是的,所以为了减少获取锁的次数,因为在调用signal之前必须要先获取对应的锁,不然会抛异常
             * 那么就设计成只获取一次或者说只出现一次获取锁,所以在take方法中就出现了这样子的代码
             *  if (c == capacity)
             *      signalNotFull();
             * 也就是说只有在饱满队列情况下去获取元素时我才会唤醒,其余情况下我压根就不管了,所以总结一点就是它只会唤醒一次,那这样子可不行啊,多个线程阻塞的情况下只会有一个线程被唤醒,其他线程可怎么办呢?
             * 所以就出现了如下的代码片段,在多次获取元素时count的值会改变,那么只要唤醒一次后,在put方法中就有机会执行下面的代码片段来唤醒其他的线程,所以这就解决问题了
             *
             * 总结:减少获取锁,实际上这也是因为读写分离所造成的代码,你看ArrayBlockingQueue就不需要
             */
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            putLock.unlock();
        }
        if (c == 0) // 跟take方法中的代码片段效果一样,只会唤醒一次notEmpty
            signalNotEmpty();
    }

    /**
     * 插入元素
     * 当队列饱满的情况下会阻塞等待指定时间,直到被唤醒或被中断或超时
     * @param e 元素
     * @param timeout 指定时间
     * @param unit 时间单位
     * @return 结果值
     */
    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)
                    return false;
                nanos = notFull.awaitNanos(nanos);
            }
            enqueue(new Node<E>(e));
            c = count.getAndIncrement();
            if (c + 1 < capacity) // 可参考put方法中的解释
                notFull.signal();
        } finally {
            putLock.unlock();
        }
        if (c == 0)
            signalNotEmpty();
        return true;
    }

    /**
     * 插入元素
     * 当队列饱满的情况下会返回false
     * @param e 元素
     * @return 结果值
     */
    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 (count.get() < capacity) {
                enqueue(node);
                c = count.getAndIncrement();
                if (c + 1 < capacity)
                    notFull.signal();
            }
        } finally {
            putLock.unlock();
        }
        if (c == 0)
            signalNotEmpty();
        return c >= 0;
    }

    /**
     * 获取元素
     * 当队列为空的情况下一直阻塞等待,直到被唤醒或被中断
     * @return 结果值
     */
    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) // 与put方法中的作用有异曲同工
                notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
        if (c == capacity)
            signalNotFull();
        return x;
    }

    /**
     * 获取元素
     * 当队列为空的情况下阻塞等待指定时间,直到被唤醒或被中断或超时
     * @param timeout 指定时间
     * @param unit 时间单位
     * @return 结果值
     */
    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)
                    return null;
                nanos = notEmpty.awaitNanos(nanos);
            }
            x = dequeue();
            c = count.getAndDecrement();
            if (c > 1)
                notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
        if (c == capacity)
            signalNotFull();
        return x;
    }

    /**
     * 获取元素
     * 当队列为空的情况下直接返回null
     * @return 结果值
     */
    public E poll() {
        final AtomicInteger count = this.count;
        if (count.get() == 0)
            return null;
        E x = null;
        int c = -1;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
            if (count.get() > 0) {
                x = dequeue();
                c = count.getAndDecrement();
                if (c > 1)
                    notEmpty.signal();
            }
        } finally {
            takeLock.unlock();
        }
        if (c == capacity)
            signalNotFull();
        return x;
    }

    /**
     * 获取元素
     * 与前面的三个方法较大不同,因为其不会导致head指向发生改变,所以该方法即使调用多次依然能获取到元素,而其他三个方法则不行
     * @return 结果值
     */
    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();
        }
    }

    /**
     * 移除指定节点,解除关联关系
     * @param p 指定节点
     * @param trail 上一个节点 
     */
    void unlink(Node<E> p, Node<E> trail) {
        p.item = null;
        trail.next = p.next;
        if (last == p)
            last = trail;
        if (count.getAndDecrement() == capacity)
            notFull.signal();
    }

    /**
     * 移除指定元素
     * @param o 指定元素
     * @return 是否移除成功
     */
    public boolean remove(Object o) {
        if (o == null) return false;
        fullyLock(); // 由于后续要遍历队列,所以应该防止其他线程修改其数据结构,否则将造成数据不一致
        try {
            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();
        }
    }

总结

LinkedBlockingQueue提供了多种插入/获取元素的方法,先简单说下区别:

  • 在队列饱满的情况下插入新元素

    • add:直接抛出异常。

    • offer:直接返回false。

    • put:一直阻塞等待直到被唤醒或线程被中断。

    • offer(time):阻塞等待指定时间,直到被唤醒或被中断或超时

  • 在队列为空的情况下获取元素

    • poll:直接返回null。

    • take:一直被阻塞等待直到被唤醒或线程被中断。

    • poll(time):阻塞等待直到被唤醒或线程被中断或超时

    • peek:直接返回null,该方法即使调用多次依然能获取到元素,而其余3个方法则不行。

与ArrayBlockingQueue完全一致...说下不同点吧,为了方便ArrayBlockingQueue将直接写成ABQ,LinkedBlockingQueue写成LBQ。

  • ABQ同一时间要么是读要么写,LBQ读写分离,所以LBQ的吞吐量比ABQ高,但预测性较差。

  • ABQ数据结构是数组有界阻塞队列,LBQ数据结构是单链表有界阻塞队列。

关于LBQ读写分离的代码与ABQ有着较大的区别,读者最好去理解下!

posted @ 2020-12-21 21:49  zliawk  阅读(67)  评论(0)    收藏  举报