上一篇文章我们介绍了 LinkedBlockingQueue 队列,我们知道 LinkedBlockingQueue 是一个无界先进先出的队列。今天我们来学习一个双端队列 – LinkedBlockingDeque,该队列可以在队首和队尾插入元素。这两种队列的底层都是基于链表实现。
下面我们来看看 LinkedBlockingDeque 类的关系图:
上一篇文章我们的就说过是接口方法是不断抽象出来,每个子接口不断拓展父接口的功能。还介绍了 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 构成一个节点)
这时我们来了个新的节点:
由于我们是从队首入队。这时,新节点的 next 会断掉指向空的节点,next 指向原来的 head 节点对象。 你可以理解为小弟取代自己的原来老大拿到第一把交椅。
这时候,旧节点指将自己的前置节点指向新入队的节点。可以理解:新老大上任,宽宏大量,让旧老大叫自己一声 “老大" 好就行了。
全部结束后,将 head 节点指向新节点,游戏结束了。
下面我们来看看出队操作,分为堵塞和非堵塞。下面仅介绍队首,队尾出队方式差不多就不看了。
堵塞出队操作:
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 构成一个节点) 节点对象:
首先先断掉指向上一个节点的前置节点,然后将前置节点指向自己(前置节点指向自己,可以帮助 GC 回收已经无用的节点)
接下来,将前一个节点的 next 指向节点设置为空。
最后,将 last 对象指向将 next 指向空的对象。OK,游戏结束。
原本还想写队尾入队,队首出队的。但是碍于篇幅有限。而且,代码也及其简单,就不写了,相信这几张图的学习以及搞定了。有兴趣可以自己尝试去画图。
LinkedBlockingDeque 作为一种阻塞双端队列,在 BlockingQueue 的基础上增加队首队尾的出入队方法。在日常开发中需要选择相关业务来选择阻塞队列。当然,打死也不能用无界队列,这玩意不可控,一旦失控就 GG。最后祝大家学习进步,工作愉快。谢谢
程序人生,与君共勉!
最近在做公众号,文章会同步发布到公众号内,如果有兴趣的同学欢迎关注公众号。