线程池中常用的阻塞队列有4种:ArrayBlockingQueue(有限队列)、LinkedBlockingQueue(无限队列)、SynchronousQueue(无空间队列)、DelayedWorkQueue(延迟优先队列)。
ArrayBlockingQueue和LinkedBlockingQueue分别以数组和链表为基础,实现有阻塞功能的队列,较为相似;SynchronousQueue没有实际容量,重点是用来匹配生产者和消费者;DelayedWorkQueue用在ScheduledThreadPoolExecutor中,用来实现延迟优先排序功能。本文重点记录前两个队列的功能和实现,SynchronousQueue和DelayedWorkQueue实现差异较大,在后续文章中记录。
add、offer、put三个方法的区别就不多说了,下文统称为入队,take、poll、remove三个方法也不多说了,下文统称为出队。
ArrayBlockingQueue,在初始化时必须指定容量,容量超过限定时入队失败。
ArrayBlockingQueue内部数组对象是一个整体,内部只有一个锁,由出队和入队公用,所以出队和入队不能同时进行。由两个索引分别指向下一个入队和出队的下标,索引指向数组最大值之后会重置为0,整体实现类似于一个环形队列,在获取锁之后才能够进行入队和出队操作,内部维护一个count来记录队列已满或为空,整体实现并不复杂。
/** The queued items */
final Object[] items;
/** items index for next take, poll, peek or remove */
int takeIndex;
/** items index for next put, offer, or add */
int putIndex;
/** Number of elements in the queue */
int count;
/*
* Concurrency control uses the classic two-condition algorithm
* found in any textbook.
*/
/** Main lock guarding all access */
final ReentrantLock lock;
/** Condition for waiting takes */
private final Condition notEmpty;
/** Condition for waiting puts */
private final Condition notFull;
LinkedBlockingQueue为无限队列,初始化时可不指定容量,此时默认容量为Integer.MAX_VALUE(称为无限队列的原因)。
LinkedBlockingQueue内部有两个锁,类似于读锁和写锁分别对入队和出队进行同步("two lock queue" algorithm
),由于链表中的每个节点相对数组更加独立,常用的出入队方法只需要获取其中的一个锁即可进行操作,(count()、contains()、tuArray()等方法需要同时获取读写锁),由于读写分离,LinkedBlockingQueue的吞吐量要大于ArrayBlockingQueue,但是,由于读写锁分离,需要解决读写之间的可见性问题。
/* Whenever an element is enqueued, the putLock is acquired and
* count updated. A subsequent reader guarantees visibility to the
* enqueued Node by either acquiring the putLock (via fullyLock)
* or by acquiring the takeLock, and then reading n = count.get();
* this gives visibility to the first n items.
*
* 当一个元素入队时,需要过去写锁,并更新count,另外一个读线程可以通过获取
* fullyLock()来获取写锁,获得写入数据的可见性,或者通过读取count值来获取
* 前n个元素的可见性,
*/
LinkedBlockingQueue的入队方法就是在获取写锁之后,新建节点,加到队尾,有一点需要注意的是,为保证最大限度的吞吐量,只有在写入前锁为空的时候才会去尝试获取读锁,来对读锁上等待的线程进行唤醒:
public boolean offer(E e) {
if (e == null) throw new NullPointerException();
final AtomicInteger count = this.count;
if (count.get() == capacity)
return false;
int c = -1;
Node<E> node = new Node<E>(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)// 队列从0到1的时候会唤醒读线程
signalNotEmpty();
return c >= 0;
}
private void enqueue(Node<E> node) {
// assert putLock.isHeldByCurrentThread();
// assert last.next == null;
last = last.next = node;
}
LinkedBlockingQueue出队操作同样在满的队列下出队才会尝试去获取写锁,进行写线程的唤醒,还有一个细节需要注意的是,LinkedBlockingQueue的head节点item值永远为空,出队虽然返回的是head.next的item值,但是删除的是head节点,并重新将head的引用只想head.next这样实现,同样是避免在读写分离的情况下,读写同时操作同一个节点,将新入队的节点挂在了已经删除的节点上。
public E poll() {
final AtomicInteger count = this.count;
if (count.get() == 0)
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;
}
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;
}
LinkedBlockingQueue中还有一个重点remove操作和对GC的支持,源码中解释如下:
/*
* To implement weakly consistent iterators, it appears we need to
* keep all Nodes GC-reachable from a predecessor dequeued Node.
* That would cause two problems:
* - allow a rogue Iterator to cause unbounded memory retention
* - cause cross-generational linking of old Nodes to new Nodes if
* a Node was tenured while live, which generational GCs have a
* hard time dealing with, causing repeated major collections.
* However, only non-deleted Nodes need to be reachable from
* dequeued Nodes, and reachability does not necessarily have to
* be of the kind understood by the GC. We use the trick of
* linking a Node that has just been dequeued to itself. Such a
* self-link implicitly means to advance to head.next.
*
* 为了实现弱一致性的迭代器,我们需要对所有节点对已经出队的前任节点
* 保持GC可达(已经出队的前任节点next扔指向queue中未出队节点)这将
* 造成两个问题:
* -允许恶意的Iterator导致无限的内存保留。
* -如果一个老节点在生命周期内进入老年带,将和一个在年轻带中的新节点
* 形成跨带引用连接,这会使正常的GC很难应对,并且造成重复的major GC。
* 然而,已出队的节点仅仅需要未出队节点的地址即可,这种连接并不一定非
* 要是那种正常的能被GC理解的引用指向的形式,我们将出队的节点next指向他
* 自己(self-link),当迭代器遇到这样的节点时,就能够理解该节点已经出
* 队,并且下一个节点应该是head.next。
*/
同时这篇文章的介绍十分清楚:
关于GC的问题
为了实现迭代器的弱一直性,我们不能直接将出队的节点的下一个节点设置为null,因为这会导致迭代器终止,但是这样就给GC带来了难度,假设一个节点X在队列中待的足够久以至于已经进入老年代,然后新创建的位于新生代的Y进入队列即X.Next = Y,这时候即使X出队了但是老年代的Major GC发生的频率较低,导致X暂时驻留老年代,紧接着Y出队,但是由于被X引用导致新生代的minor GC没有回收Y,这时候就出现了所谓的跨代引用,跨代引用造成像Y这样的新生代对象也被复制进入老年代,而且依然不能被老年代的Major GC 回收掉。所以在LinkedBlockingQueue的实现中将出队的节点的下一个节点指向自己来避免这种情况,这就是所谓的self-link。
但是,这只针对正常从头部出队的节点,通过remove的方式移除的节点它的next依然指向了另一个节点,依然会导致跨代引用GC无法回收的问题,因此一定要谨慎使用remove (object)方法。