概览和简介
LinkedBlockingDeque 是juc 包提供的一个基于链表的双向可选有界的阻塞队列。
通过把参数传给构造参数创建有界的队列可以防止过度的膨胀。没特别指定容量的话,默认最大的容量是 Integer#MAX_VALUE =2^31-1。
链表的每个节点insert时动态创建的。
如果没有阻塞,大多操作能在常数时间完成。除了 #remove(Object) ,removeFirstOccurrence,removeLastOccurrence,contains,iterator 方法和批量操作,这些操作时间复杂度是O(n)。
阻塞队列的特点是,如果队列为空,take/pop一个元素会一直等待(remove 会报错,pull会返回null,poll 看情况,get 会抛异常);如果队列已满,offer/put 一个元素也会阻塞一直等待(add 会错报容量已满)。
数据结构
数据结构本身不是很复杂,首先有一个内部类 final class Node<E> ,用于包装每个节点的数据。另外有成员属性 Node<E> first,Node<E> last,当前拥有元素数量int count ,容量 int capacity;
这下都是常见的,还有用于实现阻塞的 ReentrantLock lock 和 对应的两个Condition,notEmpty和notFull 。
Node<E>
final 的内部类,只有三个成员变量
E item 当前节点数据 Node<E> prev 指向当前节点前驱节点的地址 Node<E> next 指向当前节点下个节点的地址
常用方法
无参构造方法
无参构造方法返回的对象默认队列长度是Integer的最大值。2^32-1
构造方法 LinkedBlockingDeque(Collection<? extends E> c)
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();
}
}
容量是Integer 最大值
操作前lock.lock()
try {} finally {} 形式释放锁,
取每一个元素,依次放置的链表最后面
添加一个元素 void addFirst(E e)
if (e == null) throw new NullPointerException();
Node<E> node = new Node<E>(e);
final ReentrantLock lock = this.lock;
lock.lock();
try {
return linkFirst(node);
} finally {
lock.unlock();
}
关键的 linkFirst 方法
private boolean linkFirst(Node<E> node) {
// assert lock.isHeldByCurrentThread();
if (count >= capacity)
return false;
Node<E> f = first;
node.next = f;
first = node;
if (last == null)
last = node;
else
f.prev = node;
++count;
notEmpty.signal();
return true;
}
主要逻辑是,先判断当前容量是不是超了,然后把当前节点的的next 指向原来的first,把原first的prev指向当前节点,同时注意如果是第一个元素进来,初始化一下 last 指针。
常规链表操作完,要体现一下阻塞队列的特征,如果有线程在进行类似 take的操作,可能会阻塞等待,所以要用 Condition notEmpty 进行通知,让其可以被唤醒,进行对应操作。
boolean offerFirst(E e)/boolean offerLast(E e)
这个和add方法的区别是,容量满了没加进去,不会直接报错,只会返回 false
putFirst(E e)/putLast(E e)
这两个方法在队列已满的情况下会阻塞,这就是上面方法不同的地方。
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();
}
}
一个关键的不同就在于,如果尝试linkFirst 失败了,会进行await,等待拿掉一个元素 signal (notify的意思)。
另外有一个点,如果多个线程都在这里await 了,取出一个元素只会signal某一个线程。
boolean offerFirst/offerLast(E e, long timeout, TimeUnit unit)
在队列最前面或者最后面插入一个元素,会等待指定的时间,指定时间后还没成功,就会返回false。
取出一个元素 E removeFirst()/removeLast()
public E removeFirst() {
E x = pollFirst();
if (x == null) throw new NoSuchElementException();
return x;
}
关键方法E unlinkFirst()
private E unlinkFirst() {
// assert lock.isHeldByCurrentThread();
Node<E> f = first;
if (f == null)
return null;
Node<E> n = f.next;
E item = f.item;
f.item = null;
f.next = f; // help GC
first = n;
if (n == null)
last = null;
else
n.prev = null;
--count;
notFull.signal();
return item;
}
取出其中的第一个,然后标志这个队列肯定已经不是满的,notFull.signal(); 想要 put/offer 可以继续进行了。
E pollFirst()/pollLast()
和remove 方法差不多的,只不多不会抛空指针,会直接返回null
E pollFirst(long timeout, TimeUnit unit)/pollLast(long timeout, TimeUnit unit)
这个重载的方法设计的很奇怪,如果给了超时时间,反而会等待指定的时间,如果到了时间还没取到才会返回null.
E takeFirst()/takeLast()
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();
}
}
如果没取到会进行一直等待。
E getFirst()/getLast()
这个没什么,取到的为null就抛异常
E peekFirst()/peekLast()
这个对队列本身没有改变,知识对首尾的一次查看操作
public E peekFirst() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return (first == null) ? null : first.item;
} finally {
lock.unlock();
}
}
其他方法就不一一展开了,都比较普通。
应用场景和拓展点
JDK一个提供了7个阻塞队列
- ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。
- LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。
- PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
- DelayQueue:一个使用优先级队列实现的无界阻塞队列。
- SynchronousQueue:一个不存储元素的阻塞队列。
- LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
- LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
阻塞队列是典型的生产者消费者模式,和中间件MQ很类似,一边生产一边消费。这样做可以把生产者和消费者进行解耦。
在使用的时候,Node包装的对象,基本上是一个实现Runnable的对象,比如,创建线程池的时候有一个参数就是阻塞队列,用于保存提交过来的任务,然后线程池中的工作线程会不停的从阻塞队列中获取任务进行处理。