面试准备 -- 线程池队列LinkedBlockingDeque详解

上一篇文章我们介绍了 LinkedBlockingQueue 队列,我们知道 LinkedBlockingQueue 是一个无界先进先出的队列。今天我们来学习一个双端队列 – LinkedBlockingDeque,该队列可以在队首和队尾插入元素。这两种队列的底层都是基于链表实现。

下面我们来看看 LinkedBlockingDeque 类的关系图:
面试准备 -- 线程池队列LinkedBlockingDeque详解_第1张图片
上一篇文章我们的就说过是接口方法是不断抽象出来,每个子接口不断拓展父接口的功能。还介绍了 BlockingQueue 接口,既然 BlockingDeque 接口继承自 BlockingQueue 接口,那我们大致知道有哪些功能了。接下来看看 BlockingQueue 和 BlockingDeque 接口对比,BlockingDeque 提供了哪些功能呢?

提供功能 BlockingQueue BlockingDeque
头部入队 ×
尾部入队
头接出队
尾部部出队 ×

如上图,我们看到的从 BlockingDeque 接口比BlockingQueue 接口多了两个功能,分别是头部入队和尾部出队,放入如下:

提供功能 方法名
头部入队 putFirst()、offerFirst()、addFirst()
尾部出队 addLast()、offerLast()、putLast()
	//头结点
	transient Node<E> first;
	//尾结点
    transient Node<E> last;
    //队列中个数
    private transient int count;
    //队列长度,可以使用构造注入,如未设定,默认为无界队列
    private final int capacity;
    //显示锁
    final ReentrantLock lock = new ReentrantLock();
    //消费队列(队列为空时,无法消费,线程阻塞)
    private final Condition notEmpty = lock.newCondition();
    //生产队列(队列满时,无法入队,线程阻塞)
    private final Condition notFull = lock.newCondition();

下面我们来看看入队操作,分为堵塞和非堵塞。下面仅介绍队首、队尾入队方式差不多就不看了。
堵塞入队操作:

public void putFirst(E e) throws InterruptedException {
		//判空操作
        if (e == null) throw new NullPointerException();
        //节点对象
        Node<E> node = new Node<E>(e);
        //锁
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
        	//入队操作,如果队列满了,则线程阻塞
            while (!linkFirst(node))
                notFull.await();
        } finally {
            lock.unlock();
        }
    }

非堵塞入队操作:

 public boolean offerFirst(E e) {
 		//判空
        if (e == null) throw new NullPointerException();
        //节点对象
        Node<E> node = new Node<E>(e);
        //显示锁
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
        	//返回是否入队成功 true 为成功,false 为失败
            return linkFirst(node);
        } finally {
        	//解锁,显示锁一定要解锁。
            lock.unlock();
        }
    }

上述方法都调用同一个入队操作方法,下面我们来看看该方法的代码:

 private boolean linkFirst(Node<E> node) {
        /*
        * 判断当前队列中数量是否超过队列允许的边界
        * 如果使用默认构造,长度为 Integer.MAX_VALUE
        * 这长度等于是无界了
        */
        if (count >= capacity)
            return false;
        //获取头节点对象
        Node<E> f = first;
        //获取头结点下一个对象
        node.next = f;
        //将下一个对象设为头节点
        first = node;
        //判断尾节点是否为空对象
        if (last == null)
        	//尾节点也指向我们设置的节点
            last = node;
        else
        	//将原来的第一个节点的前置节点
        	//指向我们新入队的节点
            f.prev = node;
        //队列长度加 1 
        ++count;
        //唤醒堵塞的消费线程(task()操作)
        notEmpty.signal();
        return true;
    }

上面看不懂的话,来看看图解,算是复习下数据结构 – 链表吧!

下面是我们两个 Node(data,pre,next 构成一个节点)

面试准备 -- 线程池队列LinkedBlockingDeque详解_第2张图片
这时我们来了个新的节点:
面试准备 -- 线程池队列LinkedBlockingDeque详解_第3张图片
由于我们是从队首入队。这时,新节点的 next 会断掉指向空的节点,next 指向原来的 head 节点对象。 你可以理解为小弟取代自己的原来老大拿到第一把交椅。
面试准备 -- 线程池队列LinkedBlockingDeque详解_第4张图片

这时候,旧节点指将自己的前置节点指向新入队的节点。可以理解:新老大上任,宽宏大量,让旧老大叫自己一声 “老大" 好就行了。
面试准备 -- 线程池队列LinkedBlockingDeque详解_第5张图片
全部结束后,将 head 节点指向新节点,游戏结束了。
面试准备 -- 线程池队列LinkedBlockingDeque详解_第6张图片

下面我们来看看出队操作,分为堵塞和非堵塞。下面仅介绍队首,队尾出队方式差不多就不看了。
堵塞出队操作:

   public E takeLast() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            E x;
            //出队操作 出队成功返回出队的值,失败则为空
            while ( (x = unlinkLast()) == null)
                notEmpty.await();
            return x;
        } finally {
        //解锁
            lock.unlock();
        }
    }

非堵塞出队操作:

  public E pollLast() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
        	//出队操作 出队成功返回出队的值,失败则为空
            return unlinkLast();
        } finally {
            lock.unlock();
        }
    }

上述方法都调用同一个出队操作方法,下面我们来看看该方法的代码:

private E unlinkLast() {
        //拿到最后一个节点
        Node<E> l = last;
        //如果最后一个节点是空,则返回空
        if (l == null)
            return null;
         //拿到最后一个节点的前置节点
        Node<E> p = l.prev;
        //拿到返回值
        E item = l.item;
        //item 结节点,设空表示已经删除
        l.item = null;
        //将前置节点指向字节,
        l.prev = l; 
        //将前置节点设置为尾节点
        last = p;
        //判断最后一个 last 是否为空
        if (p == null)
            first = null;
        else
            p.next = null;
        //减少队列中数量
        --count;
        //唤醒入队的阻塞线程
        notFull.signal();
        return item;
    }

和上面一样,我们图解如何骚操作:
准备三个 Node(data,pre,next 构成一个节点) 节点对象:
面试准备 -- 线程池队列LinkedBlockingDeque详解_第7张图片
首先先断掉指向上一个节点的前置节点,然后将前置节点指向自己(前置节点指向自己,可以帮助 GC 回收已经无用的节点)
面试准备 -- 线程池队列LinkedBlockingDeque详解_第8张图片
接下来,将前一个节点的 next 指向节点设置为空。
面试准备 -- 线程池队列LinkedBlockingDeque详解_第9张图片
最后,将 last 对象指向将 next 指向空的对象。OK,游戏结束。
面试准备 -- 线程池队列LinkedBlockingDeque详解_第10张图片

原本还想写队尾入队,队首出队的。但是碍于篇幅有限。而且,代码也及其简单,就不写了,相信这几张图的学习以及搞定了。有兴趣可以自己尝试去画图。

总结:

LinkedBlockingDeque 作为一种阻塞双端队列,在 BlockingQueue 的基础上增加队首队尾的出入队方法。在日常开发中需要选择相关业务来选择阻塞队列。当然,打死也不能用无界队列,这玩意不可控,一旦失控就 GG。最后祝大家学习进步,工作愉快。谢谢

程序人生,与君共勉!

最近在做公众号,文章会同步发布到公众号内,如果有兴趣的同学欢迎关注公众号。

面试准备 -- 线程池队列LinkedBlockingDeque详解_第11张图片

你可能感兴趣的:(面试准备)