LinkedBlockingQueue是一个基于单向链表实现的可选容量的阻塞队列,队列的头节点是等待时间最长的元素,队列的尾节点是等待时间最短的元素。新元素直接插入到尾节点的后面,成为新的尾节点,队列的检索操作在队列的头部获取元素。通常情况下,链表相比数组有更高的吞吐量,但是在大多数的并发应用程序中有不可预测的性能。LinkedBlockingQueue的构造方法可以设定容量大小,不指定容量大小,默认容量是Integer的最大值。
结构
LinkedBlockingQueue使用一个单向链表来实现队列,该队列至少有一个节点,头节点不存储元素。
/**
* Node节点类
*/
static class Node<E> {
E item;
/**
* - 后继节点node=head.next
* - node为null表示head没有后继队列为空
*/
Node<E> next;
Node(E x) { item = x; }
}
/**
* 队列中的头节点
* head.item == null
*/
transient Node<E> head;
/**
* 队列的尾节点
* last.next == null
*/
private transient Node<E> last;
属性
/** 队列容量,最大值是Integer的最大值 */
private final int capacity;
/** 当前队列中元素的个数 */
private final AtomicInteger count = new AtomicInteger();
/** 出队的锁 */
private final ReentrantLock takeLock = new ReentrantLock();
/** 当队列为空时保存出队的线程 */
private final Condition notEmpty = takeLock.newCondition();
/** 入队的锁 */
private final ReentrantLock putLock = new ReentrantLock();
/** 当队列满时保存入队的线程 */
private final Condition notFull = putLock.newCondition();
LinkedBlockingQueue使用了两把锁,两个condition队列,一个是入队锁,一个是出队锁,在并发时,通过入队锁保证了线程入队列的串行化,在出队时,其中一个线程出队时,其他线程都将阻塞。虽然入队和出队同时只能有一个线程进行操作,但是可以同一时刻可以有两个线程共同操作,一个入队一个出队,所以在保证线程的时候,使用了AtomicInteger变量来表示当前队列中元素的个数,AtomicInteger是一个提供原子操作的Integer类,通过线程安全进行加减操作.这样就保证了入队和出队元素个数的一致性。相比于ArrayBlockingQueue来说,它极大了提高了吞吐量.在相同的场景下,使用LinkedBlockingQueue应该可以达到更好的效果。
构造方法
/**
* 默认构造创建一个容量是Integer的最大值的队列
*/
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
/**
* 创建一个指定容量大小的队列,初始化链表,head=last=null
*/
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node<E>(null);
}
/**
* 传入一个集合,遍历集合,当集合中的元素为null,直接抛出空指针异常,
* 元素不为空时,直接把当前元素封装成一个node节点入队,如果队列中节点个数达到
* 队列的最大容量,直接抛出异常
*/
public LinkedBlockingQueue(Collection<? extends E> c) {
this(Integer.MAX_VALUE);
final ReentrantLock putLock = this.putLock;
putLock.lock(); // Never contended, but necessary for visibility
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();
}
}
put
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
// 元素不能为null
int c = -1;
Node<E> node = new Node(e); // 以当前元素新建node节点
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly(); // 入队时使用响应中断锁
try {
// 当队列满了将线程放入condition队列等待
while (count.get() == capacity) {
notFull.await();
}
enqueue(node); // 入队操作
c = count.getAndIncrement(); // 再次获取一下入队之前的元素个数
if (c + 1 < capacity) // 如果队列还有剩余容量,唤醒等待的入队线程
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0) // 如果队列本来为空,现在加入元素后,直接唤醒出队线程
signalNotEmpty();
}
添加元素首先进行非空判断,如果为空直接抛出空指针异常,然后把当前节点封装成一个node,此时进行加锁,把并行变成串行,只有一个线程才能进行添加操作,当队列满了的时候,让当前线程释放锁,进入condition等待队列,其他线程竞争到锁,如果队列仍然是满的,也会释放锁加入到condition队列,当有线程取出队列中的一个元素时,会发出唤醒信号,condition队列上的线程重新去竞争锁,继续进行入队操作,再次获取一下节点入队之前的元素个数,如果队列还有容量,直接唤醒notfull等待队列上的线程。最后再次判断添加元素之前队列是否为空,如果为空,现在添加了元素,就可以唤醒等待出队的线程。
/**
* 把当前节点追到到队列的尾节点
*/
private void enqueue(Node<E> node) {
last = last.next = node;
}
看一下signalNotEmpty方法。
/**
* 使用take锁,释放notEmpty队列上的等待线程
*/
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
notEmpty.signal();
} finally {
takeLock.unlock();
}
}
offer方法与put的区别在于当队列满了的时候在进行添加元素直接返回false。offer(E e, long timeout, TimeUnit unit)方法和offer方法很相似,只不过是在给定时间内如果没有入队成功,直接返回false。
take
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
while (count.get() == 0) { // 队列为空将线程加入notEmpty等待队列中
notEmpty.await();
}
x = dequeue(); // 出队
c = count.getAndDecrement(); // 再次获取一下出队之前的元素个数
if (c > 1) // 如果队列中还有元素,释放notEmpty等待队列上的线程
notEmpty.signal();
} finally {
takeLock.unlock();
}
if (c == capacity) // 再出队之前队列满了直接唤醒入队线程
signalNotFull();
return x;
}
take和put的过程很相似,当队列为空,将线程加入到notEmpty的等待队列中,当队列不为空,就进行出队操作,再次确定一下出队前的元素个数,如果元素个数>1,唤醒notEmpty等待队列上的线程。最后再判断一下,如果队列本来就满了,现在正好取出了一个元素,唤醒入队的线程。
/**
* 出队时取队列的最前面的元素,要注意头节点是一个空节点
*/
private E dequeue() {
Node<E> h = head;
Node<E> first = h.next;
h.next = h; // 将当前头节点的引用指向自己,便于gc
head = first; // 将头节点的next作为头节点,返回该节点的元素,并把将节点置为空
E x = first.item;
first.item = null;
return x;
}
/**
* 使用put锁,唤醒notfull队列上的线程
*/
private void signalNotFull() {
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
notFull.signal();
} finally {
putLock.unlock();
}
}
poll方法与take相比的不同在于当队列为空时直接返回null。poll(long timeout, TimeUnit unit)则是在指定的时间内没有获取到元素直接返回null。
peek
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();
}
}
peek是返回队列中head的下一个元素,并不是取出。
put和take可以允许两个线程在链表两端同时进行入队和出队操作,但是一端只能有一个线程执行,通过创建了两把锁来进行线程安全。入队和出队是相互联系的:当多线程中执行入队操作时,a线程抢占了putlock,添加了一个元素,此时正好队列满了,a线程释放锁,线程b获取了锁,但是马上释放锁,加入到notFull的等待队列,线程c加入到了notFull等待队列上b的后面。这时一个线程取出了一个元素,调用signalNotNull方法,通知notFull等待队列,线程b获取了锁,插入一个元素,如果这时检查发现还可以再添加一个元素,调用signal,唤醒c插入元素。出队与此过程相同。
remove
public boolean remove(Object o) {
if (o == null) return false; // 要查找的元素为null,直接返回false
fullyLock(); // 要同时上两把锁
try {
for (Node<E> trail = head, p = trail.next;
p != null;
trail = p, p = p.next) { // 从头节点的下一个节点遍历
if (o.equals(p.item)) { // 如果找到直接移除,返回true
unlink(p, trail);
return true;
}
}
return false; // 没找到返回false
} finally {
fullyUnlock();
}
}
remove操作的时候要同时上两把锁,这是因为要从头往后遍历,涉及到队列的两端,需要对两端加锁。
void unlink(Node<E> p, Node<E> trail) {
p.item = null; // 把p置为null
trail.next = p.next; // 改变p的连接,便于gc
if (last == p) // 如果删除的是尾节点
last = trail; // 直接把p的前节点设为尾节点
if (count.getAndDecrement() == capacity) // 如果要删除前的队列满了,直接唤醒notfull等待队列上的线程
notFull.signal();
}