概述
基于单链表的有界或无界阻塞队列。
1)FIFO;
2)可用带容量参数的构造函数设置队列的容量,否则为Integer.MAX_VALUE;
3)在大部分并发场景中,基于链表的队列比基于数组的队列有更高的吞吐量,但性能的预测性更低。
数据结构
单链表,一把take锁,一个take条件,一把put锁,一个put条件:
// 链表首节点
// head.item == null
private transient Node<E> head;
// 链表尾节点
// last.next == null
private transient Node<E> last;
// takeLock
private final ReentrantLock takeLock = new ReentrantLock();
// 队列为空时,在takeLock上的等待
private final Condition notEmpty = takeLock.newCondition();
// putLock
private final ReentrantLock putLock = new ReentrantLock();
// 队列已满时,在putLock上的等待
private final Condition notFull = putLock.newCondition();
static class Node<E> {
E item;
// 取值有三种情况:
// next为后继节点或this(表明后继节点为head.next)或null(表明其为链表尾节点)
Node<E> next;
Node(E x) { item = x; }
}
构造器
// 无参构造
// 无界链表:Integer.MAX_VALUE
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
// 带容量参数构造
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node<E>(null);
}
// 带Collection参数构造,将其中元素入队
// 无界链表:Integer.MAX_VALUE
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();
}
}
增删查
基础方法
// notEmpty signal,在put/offer中调用
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
notEmpty.signal();
} finally {
takeLock.unlock();
}
}
// notFull signal,在take/poll中调用
private void signalNotFull() {
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
notFull.signal();
} finally {
putLock.unlock();
}
}
// 节点入队
private void enqueue(Node<E> node) {
// assert putLock.isHeldByCurrentThread();
// assert last.next == null;
last = last.next = node;
}
// 将链表首节点出队
private E dequeue() {
// assert takeLock.isHeldByCurrentThread();
// assert head.item == null;
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;
}
增
步骤(注意:offer非阻塞版,在获取putLock前先检测队列是否已满,若已满则立即返回false,否则获取putLock):
1)获取putLock;
2)若队列未满,则才将节点入队,成功后若队列还未满,向put/offer线程发送notFull信号;
3)若队列已满,put一直等到队列未满,offer阻塞版则等待timeout时间,超出则立即返回false;
4)释放putLock锁。
5)若入队成功且c==0,则获取takeLock锁,向take/poll线程发送notEmpty信号,再释放takeLock锁。
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
int c = -1; // 用局部变量标识结果,若返回前为-1,则表明操作失败。put/take线程都采用该方法。
Node<E> node = new Node(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly(); // 获取到putLock锁前,可中断的锁的获取
try {
while (count.get() == capacity) { // 队列已满,则一直wait
notFull.await();
}
enqueue(node); // 将节点入队
c = count.getAndIncrement();
if (c + 1 < capacity) // 队列仍未满
notFull.signal(); // 向其他put/offer线程发送notFull信号
} finally {
putLock.unlock(); // 释放putLock
}
if (c == 0) // 入队成功
signalNotEmpty(); // 获取takeLock锁,向take/poll线程发送notEmpty信号,再释放takeLock锁。
}
删
与增的过程对立。
步骤(注意:poll非阻塞版,在获取takeLock前先检测队列是否为空,若为空则立即返回null,否则获取takeLock):
1)获取takeLock;
2)若队列非空,则将链表首节点出队,成功后若队列还未空,向take/poll线程发送notEmpty信号;
3)若队列为空,take一直等到队列未满,poll阻塞版则等待timeout时间,超出则立即返回null;
4)释放takeLock锁。
5)若出队成功且c==capacity,则获取putLock锁,向put/offer线程发送notFull信号,再释放putLock锁。
public E take() throws InterruptedException {
E x;
int c = -1; // 用局部变量标识结果,若返回前为-1,则表明操作失败。
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly(); // 获取到takeLock锁前,可中断的锁的获取
try {
while (count.get() == 0) { // 队列为空,则一直wait
notEmpty.await();
}
x = dequeue(); // 将链表首节点出队
c = count.getAndDecrement();
if (c > 1) // 队列仍未满
notEmpty.signal(); // 向其他take/poll线程发送notEmpty信号
} finally {
takeLock.unlock(); // 释放takeLock
}
if (c == capacity) // 出队成功
signalNotFull(); // 获取putLock锁,向put/offer线程发送notFull信号,再释放putLock锁。
return x;
}
查
步骤:
1)先检测队列是否为空,若为空则立即返回null;
2)否则获取takeLock;
3)获取链表首节点,若为null,则返回null;否则返回item值;
4)释放takeLock锁。
// 获取链表首元素
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();
}
}
迭代器
弱一致性。
private class Itr implements Iterator<E> {
/*
* Basic weakly-consistent iterator. At all times hold the next
* item to hand out so that if hasNext() reports true, we will
* still have it to return even if lost race with a take etc.
*/
private Node<E> current;
private Node<E> lastRet;
private E currentElement;
Itr() {
fullyLock();
try {
current = head.next;
if (current != null)
currentElement = current.item;
} finally {
fullyUnlock();
}
}
public boolean hasNext() {
return current != null;
}
/**
* Returns the next live successor of p, or null if no such.
*
* Unlike other traversal methods, iterators need to handle both:
* - dequeued nodes (p.next == p)
* - (possibly multiple) interior removed nodes (p.item == null)
*/
private Node<E> nextNode(Node<E> p) {
for (;;) {
Node<E> s = p.next;
if (s == p)
return head.next;
if (s == null || s.item != null)
return s;
p = s;
}
}
public E next() {
fullyLock();
try {
if (current == null)
throw new NoSuchElementException();
E x = currentElement;
lastRet = current;
current = nextNode(current);
currentElement = (current == null) ? null : current.item;
return x;
} finally {
fullyUnlock();
}
}
public void remove() {
if (lastRet == null)
throw new IllegalStateException();
fullyLock();
try {
Node<E> node = lastRet;
lastRet = null;
for (Node<E> trail = head, p = trail.next;
p != null;
trail = p, p = p.next) {
if (p == node) {
unlink(p, trail);
break;
}
}
} finally {
fullyUnlock();
}
}
}
特性
实际上put/offer操作与take/poll操作是通过两把锁、两个条件:putLock、notFull、takeLock、notEmpty来进行协作的,来实现LinkedBlockingQueue的功能。
LinkedBlockingQueue的读与写如何保证可见性?
当节点入队时,需要获取putLock,count增加,后续的读操作可以通过获取putLock锁(通过获取全锁:putLock+takeLock)或获取takeLock锁,读n = count.get(),根据happens-before原则,这样就可以读取到新入队的n个节点。
疑问
为什么其更高的吞吐量,但性能的预测性更低?