上一篇文章笔主介绍了BlockingQueue,了解了什么是阻塞队列,阻塞队列的支持的操作以及阻塞队列的应用场景。现在,我们来学习下BlockingQueue的实现类之一LinkedBlockingQueue。
还是老套路,我们看下JDK中对LinkedBlockingQueue的定义,如下:
An optionally-bounded {@linkplain BlockingQueue blocking queue} based on linked nodes. This queue orders elements FIFO (first-in-first-out). The head of the queue is that element that has been on the queue the longest time. The tail of the queue is that element that has been on the queue the shortest time. New elements are inserted at the tail of the queue, and the queue retrieval operations obtain elements at the head of the queue. Linked queues typically have higher throughput than array-based queues but less predictable performance in most concurrent applications.
The optional capacity bound constructor argument serves as a way to prevent excessive queue expansion. The capacity, if unspecified, is equal to {@link Integer#MAX_VALUE}. Linked nodes are dynamically created upon each insertion unless this would bring the queue above capacity.
LinkedBlockingQueue是基于链表实现可以设置大小的阻塞队列。这个队列要求元素“先进先出”,队列头部的元素在队列中存活时间最长,队列尾部的元素在队列中存活时间最短。新元素会被插入队列的尾部,而且移除元素时从队列的头部进行遍历操作。链接队列通常比基于数组的队列具有更高的吞吐量,但在大多数并发应用中其性能难以预测。
可选容量的构造方法用来防止队列过度扩容。如果不指定容量,那么队列的容量等于Integer能表示的最大值。链接节点在每次插入时会动态创建,除非已经超过了队列指定的容量。
简单总结下,LinkedBlockingQueue有以下特点:
接下来,我们从源码的角度来认识下LinkedBlockingQueue。
/**
* 使用静态内部类定义链表节点
*/
static class Node {
E item; //节点中封装的数据,使用泛型
Node next; //当前节点的下一个节点
Node(E x) { item = x; } //节点的构造方法
}
//队列的容量,如果初始化时不指定,默认容量为Integer.MAX_VALUE
private final int capacity;
//使用AtomicInteger类来创建一个原子计数器,表示当前队列中的元素个数
private final AtomicInteger count = new AtomicInteger();
//队列的头部节点,头部节点中的数据永远为null
transient Node head;
//队列的尾部节点,尾部节点的下一个节点永远为null
private transient Node last;
//使用take()、poll()方式移除元素时需要获得此锁
private final ReentrantLock takeLock = new ReentrantLock();
//非空条件对象,队列为空时移除元素的线程会被阻塞
private final Condition notEmpty = takeLock.newCondition();
//使用put()、offer()方式插入元素时需要获取此锁
private final ReentrantLock putLock = new ReentrantLock();
//非满条件对象,队列为满时插入元素的线程会被阻塞
private final Condition notFull = putLock.newCondition();
构造方法的源码如下:
//初始化时不指定队列的容量,默认容量为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(null); //初始化队列头部节点和尾部节点
}
//将给定集合的元素初始化至队列
public LinkedBlockingQueue(Collection extends E> c) {
//队列容量为Integer.MAX_VALUE
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));
++n; //计数器
}
//设置队列中的元素个数
count.set(n);
} finally {
putLock.unlock(); //释放锁
}
}
这里公共方法是指在类内部会被多次调用的方法或者是获取队列属性的方法,例如:唤醒操作(插入或移除)阻塞线程的方法、真正向队列中插入节点(enqueue)的方法、真正从队列中移除节点(dequeue)的方法、整个队列加锁的方法、整个队列释放锁的方法、获取队列元素个数的方法、清空整个队列的方法、将队列转换为数组的方法等等。源码如下:
//唤醒阻塞的获取元素线程,该方法只能在put()、offer()中被调用
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
//需要注意,await()、signal()方法必须在加锁的环境中使用
notEmpty.signal();
} finally {
takeLock.unlock();
}
}
//唤醒阻塞的插入元素线程,该方法只能在take()、poll()中被调用
private void signalNotFull() {
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
notFull.signal();
} finally {
putLock.unlock();
}
}
//在队列尾部插入元素
private void enqueue(Node node) {
/**
* 以下赋值语句相当于:
* last.next = node;
* last = last.next;
*/
last = last.next = node;
}
//从队列的头部移除一个元素
private E dequeue() {
//获取头部节点,注意头部节点的数据永远为null
Node h = head;
//获取头部节点的下一个节点,也就是第一个数据节点
Node first = h.next;
//h.next指向自身,这样h.next与GC Roots没有可达路径,下次GC时会被回收
h.next = h; // help GC
//指定新的头部节点
head = first;
//获取头部节点的数据
E x = first.item;
//使头部节点数据重新变为null
first.item = null;
//返回头部节点数据
return x;
}
//整个队列加锁,不能在尾部插入元素也不能从头部移除元素
void fullyLock() {
putLock.lock();
takeLock.lock();
}
//整个队列释放锁
void fullyUnlock() {
takeLock.unlock();
putLock.unlock();
}
//获取队列中的元素个数
public int size() {
return count.get();
}
//返回队列还可以容纳的元素个数
public int remainingCapacity() {
return capacity - count.get();
}
/**
* 如果队列为空,直接返回null
* 否则,从队列头部节点查询元素。注意,此处仅仅是查询元素,并未从队列中移除。
*/
public E peek() {
if (count.get() == 0)
return null;
final ReentrantLock takeLock = this.takeLock;
takeLock.lock(); //获取锁
try {
Node first = head.next;
if (first == null)
return null;
else
return first.item;
} finally {
takeLock.unlock(); //释放锁
}
}
//寻找队列中是否存在数据等于o的节点
public boolean contains(Object o) {
if (o == null) return false;
fullyLock(); //同时获取插入锁和移除锁
try {
for (Node p = head.next; p != null; p = p.next)
if (o.equals(p.item)) //找到节点
return true;
return false;
} finally {
fullyUnlock(); //同时释放插入锁和移除锁
}
}
//将队列转换为Object数组
public Object[] toArray() {
fullyLock(); //同时获取插入锁和移除锁
try {
int size = count.get(); //获取队列的元素个数
//创建数组
Object[] a = new Object[size];
int k = 0;
for (Node p = head.next; p != null; p = p.next)
a[k++] = p.item; //将队列中的元素放入数组
return a;
} finally {
fullyUnlock(); //同时释放插入锁和移除锁
}
//将队列转换为T类型数组
public T[] toArray(T[] a) {
fullyLock(); //同时获取插入锁和移除锁
try {
int size = count.get(); //获取队列中元素个数
if (a.length < size)
a = (T[])java.lang.reflect.Array.newInstance
(a.getClass().getComponentType(), size);
int k = 0;
for (Node p = head.next; p != null; p = p.next)
a[k++] = (T)p.item; //将队列中的元素放入数组
if (a.length > k)
a[k] = null;
return a;
} finally {
fullyUnlock(); //同时释放插入锁和移除锁
}
}
//使用字符串表示队列中的元素
public String toString() {
fullyLock(); //同时获取插入锁和移除锁
try {
Node p = head.next;
if (p == null)
return "[]";
StringBuilder sb = new StringBuilder();
sb.append('[');
for (;;) {
E e = p.item;
sb.append(e == this ? "(this Collection)" : e); //将元素追加到StringBuilder对象中
p = p.next;
if (p == null)
return sb.append(']').toString();
sb.append(',').append(' ');
}
} finally {
fullyUnlock(); //同时释放插入锁和移除锁
}
}
//清空队列
public void clear() {
fullyLock();//同时获取插入锁和移除锁
try {
for (Node p, h = head; (p = h.next) != null; h = p) {
h.next = h; //h.next指向自身,这样h.next与GC Roots没有可达路径,下次GC时会被回收
p.item = null; //数据置为null
}
head = last; //使头结点等于尾节点
if (count.getAndSet(0) == capacity)
notFull.signal();
} finally {
fullyUnlock(); //同时释放插入锁和移除锁
}
}
//批量将队列中的元素移除并放入集合中
public int drainTo(Collection super E> c) {
return drainTo(c, Integer.MAX_VALUE);
}
//批量将队列中的元素移除并放入集合中
public int drainTo(Collection super E> c, int maxElements) {
if (c == null)
throw new NullPointerException();
if (c == this)
throw new IllegalArgumentException();
if (maxElements <= 0)
return 0;
boolean signalNotFull = false;
final ReentrantLock takeLock = this.takeLock;
takeLock.lock(); //获取锁
try {
//获取Integer.MAX_VALUE和队列元素个数中的最小值
int n = Math.min(maxElements, count.get());
Node h = head;
int i = 0;
try {
while (i < n) {
Node p = h.next;
//将队列中的元素放入集合
c.add(p.item);
p.item = null; //数据置为null
h.next = h; //h.next指向自身,这样h.next与GC Roots没有可达路径,下次GC时会被回收
h = p;
++i;
}
return n;
} finally {
if (i > 0) {
head = h;
signalNotFull = (count.getAndAdd(-i) == capacity);
}
}
} finally {
takeLock.unlock(); //释放锁
if (signalNotFull)
signalNotFull(); //唤醒阻塞的插入线程
}
}
//返回队列的迭代器对象,通过私有内部类实现
public Iterator iterator() {
return new Itr();
}
LinkedBlockingQueue插入操作提供了put(E e)、offer(E e, long timeout, TimeUnit unit)、offer(E e)这三个方法,这三个方法的特点如下:
这三个方法的源码如下:
//如果队列已满,插入操作会阻塞直至队列有空闲位置
public void put(E e) throws InterruptedException {
//待插入的元素不能为空
if (e == null) throw new NullPointerException();
int c = -1;
Node node = new Node(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly(); //获取锁
try {
//如果队列已满,阻塞当前插入操作
while (count.get() == capacity) {
notFull.await(); //阻塞
}
//阻塞结束在队列尾部插入元素
enqueue(node);
//获取插入元素前队列的元素个数
c = count.getAndIncrement();
//队列未满
if (c + 1 < capacity)
//唤醒插入阻塞的线程
notFull.signal();
} finally {
putLock.unlock(); //释放锁
}
if (c == 0)
signalNotEmpty();
}
//如果队列已满,插入操作在给定的时间内会阻塞
public boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException {
if (e == null) throw new NullPointerException();
long nanos = unit.toNanos(timeout);
int c = -1;
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly(); //获取锁
try {
while (count.get() == capacity) {
if (nanos <= 0) //阻塞结束,队列还是已满状态,返回false
return false;
//插入操作给定的时间内会阻塞
nanos = notFull.awaitNanos(nanos);
}
//阻塞结束在队列尾部插入元素
enqueue(new Node(e));
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock(); //释放锁
}
if (c == 0)
signalNotEmpty();
return true;
}
/**
* 如果队列已满,返回false
* 否则,在队列尾部执行插入操作
*/
public boolean offer(E e) {
if (e == null) throw new NullPointerException();
final AtomicInteger count = this.count;
if (count.get() == capacity) //队列已满,直接返回false
return false;
int c = -1;
Node node = new Node(e);
final ReentrantLock putLock = this.putLock;
putLock.lock(); //获取锁
try {
if (count.get() < capacity) { //插入前再判断队列是否已满
enqueue(node); //在队列尾部插入元素
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
}
} finally {
putLock.unlock(); //释放锁
}
if (c == 0)
signalNotEmpty();
return c >= 0; //插入成功返回true,否则返回false
}
LinkedBlockingQueue操作移除提供了 take()、poll(long timeout, TimeUnit unit)、poll()、remove(Object o) 这四个方法,这四个方法的特点如下:
这四个方法的源码如下:
//如果队列为空,移除操作会阻塞直至队列中有元素
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.await();
}
x = dequeue(); //阻塞结束,从队列头部节点移除元素
c = count.getAndDecrement();
//队列非空
if (c > 1)
notEmpty.signal(); //唤醒移除操作阻塞的线程
} finally {
takeLock.unlock(); //释放锁
}
if (c == capacity)
signalNotFull();
return x;
}
//如果队列为空,移除操作在给定的时间会阻塞
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
E x = null;
int c = -1;
long nanos = unit.toNanos(timeout);
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly(); //获取锁
try {
while (count.get() == 0) {
if (nanos <= 0) //阻塞结束,队列还是为空直接返回null
return null;
//获取操作在给定的时间会阻塞
nanos = notEmpty.awaitNanos(nanos);
}
//阻塞结束,从队列头部节点移除元素
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock(); //释放锁
}
if (c == capacity)
signalNotFull();
return x;
}
/**
* 如果队列为空,直接返回null
* 否则,从队列头部节点移除元素
*/
public E poll() {
final AtomicInteger count = this.count;
if (count.get() == 0) //队列为空直接返回null
return null;
E x = null;
int c = -1;
final ReentrantLock takeLock = this.takeLock;
takeLock.lock(); //获取锁
try {
if (count.get() > 0) {
//队列非空,从队列头部节点移除元素
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
}
} finally {
takeLock.unlock(); //释放锁
}
if (c == capacity)
signalNotFull();
return x;
}
//根据节点p和节点p的前一个节点trail将节点p从队列中删除
void unlink(Node p, Node trail) {
//将节点p的数据置为null
p.item = null;
//将节点p的前一个节点trail的next指向节点p的next,意味着将节点p从队列中删除
trail.next = p.next;
if (last == p) //如果p是最后一个节点,将last指向trail
last = trail;
//从队列中删除元素后,唤醒阻塞的插入线程
if (count.getAndDecrement() == capacity)
notFull.signal();
}
//从队列中删除元素
public boolean remove(Object o) {
if (o == null) return false;
fullyLock(); //同时获取插入锁和移除锁
try {
//通过循环寻找数据等于o的节点
for (Node trail = head, p = trail.next;
p != null;
trail = p, p = p.next) {
if (o.equals(p.item)) {
unlink(p, trail); //找到待删除的节点p,将节点p从队列中删除
return true;
}
}
return false;
} finally {
fullyUnlock(); //同时释放插入锁和移除锁
}
}
在上文分析LinkedBlockingQueue类的源码时,有讲到iterator(),这个方法的返回值是队列的迭代器Itr类的实例。我们知道使用迭代器可以遍历整个队列,依次访问队列中的每个元素,当然LinkedBlockingQueue类的迭代器也是按照从头到尾的顺序进行遍历。下面我们通过源码来看下Itr类的具体实现,源码如下:
/ **
* Itr实现了Iterator接口,为遍历队列提供了具体实现
*/
private class Itr implements Iterator {
//当前节点对象
private Node current;
//最近访问节点对象
private Node lastRet;
//当前节点对象中的数据
private E currentElement;
//无参数构造方法
Itr() {
fullyLock(); //同时获取插入锁和移除锁
try {
current = head.next; //获取头结点的下一个节点,也就是队列中的第一个节点
if (current != null)
//当前节点不为null,就把当前节点的数据赋给currentElement
currentElement = current.item;
} finally {
fullyUnlock(); //同时释放插入锁和移除锁
}
}
//判断队列中是否还有下一个节点
public boolean hasNext() {
return current != null;
}
//获取队列中下一个节点
private Node nextNode(Node p) {
for (;;) {
Node s = p.next; //获取节点p的下一个节点
if (s == p) //如果p==s,就获取头结点的下一个节点,也就是队列中的第一个节点
return head.next;
if (s == null || s.item != null)
// 第一种情况:s == null,说明队列已经遍历完毕,没有节点可以遍历
// 第二种情况:s.item != null,说明正在遍历有效的节点
return s;
// 说明出现了第三种情况:s != null,但是s.item == null
// 说明正在遍历的节点要么是head节点,要么是last节点,就把s赋给p
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();//同时释放插入锁和移除锁
}
}
//删除队列中的节点,使用remove()方法前一定是先使用了next()方法。否则,lastRet对象为null,会抛出异常
public void remove() {
if (lastRet == null)
throw new IllegalStateException();
fullyLock(); //同时获取插入锁和移除锁
try {
Node node = lastRet; //把最近访问的节点赋给node
lastRet = null; //使lastRet为null
for (Node trail = head, p = trail.next;
p != null;
trail = p, p = p.next) {
if (p == node) {
unlink(p, trail); //找到node节点,将节点node从队列中删除
break; //跳出循环
}
}
} finally {
fullyUnlock(); //同时释放插入锁和移除锁
}
}
}
总体来看,LinkedBlockingQueue的迭代器实现思路还是比较清晰的,current和currentElement分别指向了当前节点对象和当前节点对象中的数据,lastRet指向了最近访问的节点对象。实现思路可以概括如下:
至此,我们通过分析源码分方式学习了LinkedBlockingQueue。再次总结下其特点:
那么,LinkedBlockingQueue与ArrayBlockingQueue相比,有什么不同之处?
由于笔主水平有限,笔误或者不当之处还请批评指正。