LinkedBlockingDeque是一个基于链表的双端阻塞队列。和LinkedBlockingQueue类似,区别在于该类实现了Deque接口,而LinkedBlockingQueue实现了Queue接口。该类的继承关系如下图:
本文将与LinkedBlockingQueue进行比较,关于LinkedBlockingQueue可以参考:深入理解阻塞队列(三)——LinkedBlockingQueue源码分析
概述
容量问题
LinkedBlockingDeque是一个可选容量的阻塞队列,如果没有设置容量,那么容量将是Int的最大值。
底层数据结构
LinkedBlockingDeque的底层数据结构是一个双端队列,该队列使用链表实现,其结构图如下:
源码分析
重要字段
LinkedBlockingDeque的重要字段有如下几个:
//队列的头节点
transient Node<E> first;
//队列的尾节点
transient Node<E> last;
//队列中元素的个数
private transient int count;
//队列中元素的最大个数
private final int capacity;
//锁
final ReentrantLock lock = new ReentrantLock();
//队列为空时,阻塞take线程的条件队列
private final Condition notEmpty = lock.newCondition();
//队列满时,阻塞put线程的条件队列
private final Condition notFull = lock.newCondition();
从上面的字段,可以得到LinkedBlockingDeque内部只有一把锁以及该锁上关联的两个条件,所以可以推断同一时刻只有一个线程可以在队头或者队尾执行入队或出队操作。可以发现这点和LinkedBlockingQueue不同,LinkedBlockingQueue可以同时有两个线程在两端执行操作。
构造方法
LinkedBlockingDeque的构造方法有三个,如下:
public LinkedBlockingDeque() {
this(Integer.MAX_VALUE);
}
public LinkedBlockingDeque(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
}
public LinkedBlockingDeque(Collection<? extends E> c) {
this(Integer.MAX_VALUE);
final ReentrantLock lock = this.lock;
lock.lock(); // Never contended, but necessary for visibility
try {
for (E e : c) {
if (e == null)
throw new NullPointerException();
if (!linkLast(new Node<E>(e)))
throw new IllegalStateException("Deque full");
}
} finally {
lock.unlock();
}
}
可以看到这三个构造方法的结构和LinkedBlockingQueue是相同的。 但是LinkedBlockingQueue是存在一个哨兵节点维持头节点的,而LinkedBlockingDeque中是没有的。
入队、出队方法
由于LinkedBlockingDeque是一个双端队列,所以就可以在队头执行入队和出队操作,也可以在队尾执行入队和出队操作,不过实现的方法基本类似,下面就以putFirst()为例,说明一下阻塞方法的执行过程:
public void putFirst(E e) throws InterruptedException {
//不允许元素为null
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();
}
}
从上面的代码可以看到offerFirst()的流程:
- 不允许元素为null
- 首先获取锁,一旦获取到锁后,调用linkFirst()将节点插入在队头,最后释放锁。
linkFirst()的实现如下:
private boolean linkFirst(Node<E> node) {
// 如果容量满了
if (count >= capacity)
return false;
//插入节点,将first指向新建节点
Node<E> f = first;
node.next = f;
first = node;
if (last == null)
last = node;
else
f.prev = node;
++count;
//因为插入了一个元素,通知因元素为0时阻塞的take线程
notEmpty.signal();
return true;
}
从上面可以看到,返回false只有在队列中元素满了的情况下;其他情况都会返回true,并且由于成功插入了一个节点,会调用notEmpty条件的signal()方法释放因队列中元素个数为0时的take线程。 关于一把锁,两个条件的实现方式和ArrayBlockingQueue的原理一样,可以参考深入理解阻塞队列(二)——ArrayBlockingQueue源码分析,这儿就不再过多介绍了。
设想
在分析完LinkedBlockingQueue的源码之后,再看LinkedBlockingDeque源码之前,我在想LinkedBlockingDeque可能是如何实现的?
我的想法是两把锁+四个条件,两把锁中,一把管理队头,一把管理队尾,每把锁两个条件,分别是notEmpty和notFull,这样的话,就可以同时有两个线程可以同时在队列两端执行入队和出队操作,为了实现同步,借助于一个AtomicInteger的count变量保存LinkeBlockingDeque中元素的个数。
但是Java的源码中,并不是像我这样的思路,而是使用一把锁+两个条件,这种实现方式是和ArrayBlockingQueue一样的。然后我就在想,上面的想法是否可以实现?
细想了一下,我的这种想法是不可行的,不然Java也有可能采取这种方式了。
举个例子:现在线程A假设调用putFirst(),不过队列容量满了,所以线程A就阻塞了,这是一个线程B调用了putLast(),同样由于队列容量满了,线程B也阻塞了,这时一个线程C调用了takeLast()取走了一个元素,那么该线程就可以通知尾部的锁上的notFull,这样线程B就可以释放调用putLast了,而如果想要释放线程A,只有两个方法:
- 就是线程C在调用takeLast()方法中取走一个数据时,也通知头部锁上的notFull,这也就得意味着takeLast也得占有头部锁,即占有头和尾两把锁
- 如果takeLast()只占有一把尾部锁的话,那么想要释放线程A的话,就只能希望有一个线程D调用takeFirst()取走一个头元素时通知头锁的notFull条件释放线程A
如果使用第一种方式,那么其实和使用一把锁是相同的,因为不管是队头还是队尾的入队和出队,都得先获取两把锁,最后释放两把锁,这样的实现方式是可以的;但是如果采用第二种方式,那么就会有问题,比如说:
现在队列容量还有最后一个元素可以插入,这时线程A执行putFirst()方法,线程B执行putLast()方法,当两个线程进入while循环处判断AtomicInteger的值的时候,都通过了,那么线程A会调用linkFirst将节点插入到前端,然后将count+1,线程B会调用linkLast将节点插入到尾部,然后将count+1,这时由于两个线程各插入了一个元素,那么该队列中的元素就超过了容量了,所以说第二种方式是不可行的。归根到底是因为可能存在两个线程同时对AtomicInteger做一个方向的操作,比如说都+1,都-1,而LinkedBlockingQueue可行是因为虽然会有两个线程对AtomicInteger操作,但是方向是相反的,一个+1,一个-1。
而第一种方式其实和使用一把锁是相同的,所以Java源码采用了一把锁+两个条件的方式。
总结
LinkedBlockingDeque和LinkedBlockingQueue的相同点在于:
- 基于链表
- 容量可选,不设置的话,就是Int的最大值
和LinkedBlockingQueue的不同点在于:
- 双端链表和单链表
- 不存在哨兵节点
- 一把锁+两个条件
LinkedBlockingDeque和ArrayBlockingQueue的相同点在于:使用一把锁+两个条件维持队列的同步。
到底为此,关于阻塞队列系列就到这儿完结了。
关注我的技术公众号,不定期会有优质技术文章推送。
微信扫一扫下方二维码即可关注: