在线程池创建的时候,需要传一个堵塞队列来维护需要执行的线程任务,其中最常用的是ArrayBlockingQueue和LinkedBlockingQueue。他们都继承了BlockingQueue接口。
ArrayBlockingQueue
一个有边界的堵塞队列,内部使用了一个队列来保存元素,有takeIndex和putIndex来维护队列头和尾部的游标。
/** 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;
/** Main lock guarding all access */
//在取和存元素的时候会进行加锁
final ReentrantLock lock;
/** Condition for waiting takes */
//在take的时候,如果数组为空,进行堵塞,直到数组不为空
private final Condition notEmpty;
/** Condition for waiting puts */
//在poll的时候,如果数组满了,进行堵塞,直到数组有空位
private final Condition notFull;
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
//使用锁,默认的会使用ReentrantLock的公平锁
//公平锁不先尝试获取锁,直接放入AQS等待队列里,不公平锁会先尝试是否可以获取锁,失败才会进入等待队列(具体实现可以查看ReentrantLock源码)
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
添加数据可以使用offer和put方法,两个区别是offer是实现Queue的方法,在队列满时候返回false,不进行其他操作。而put是实现了父类BlockingQueue的方法,在队列满的时候会通过notFull.await();进行堵塞,知道队列有空位的时候后再进行判断。
删除也是同理,poll是实现Queue的方法,在队列为空的时候不进行任何操作,take是实现BlockingQueue的方法,在队列空的的时候会进行堵塞。
public boolean offer(E e) {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lock();
try {
if (count == items.length)
return false;
else {
enqueue(e);
return true;
}
} finally {
lock.unlock();
}
}
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
//注意这个while,要不堵塞后还是需要进行判断
while (count == items.length)
notFull.await();
enqueue(e);
} finally {
lock.unlock();
}
}
//取出数据
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return (count == 0) ? null : dequeue();
} finally {
lock.unlock();
}
}
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
当队列添加数据的时候,先会上锁,然后调用enqueue方法添加元素。
当队列删除数据的时候,还是会先上锁,然后调用dequeue删除元素,其中会把items[takeIndex]置为null,这样有助于gc,防止内存溢出。
其中putIndex和takeIndex和最大的长度一致时,会变为0,形成一个环,使队列可以循环使用。
private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0;
count++;
notEmpty.signal();
}
private E dequeue() {
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
final Object[] items = this.items;
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length)
takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
notFull.signal();
return x;
}
LinkedBlockingQueue
LinkedBlockingQueue和ArrayBlockingQueue相比最大的不同是他可以作为一个无边界的堵塞队列,他的内部使用的链表的形式来保存元素。
//内部定义了一个很简单的Node节点来保存数据。
static class Node<E> {
E item;
/**
* One of:
* - the real successor Node
* - this Node, meaning the successor is head.next
* - null, meaning there is no successor (this is the last node)
*/
Node<E> next;
Node(E x) { item = x; }
}
其次,其内部分别定义了读写锁来对写和读进行加锁操作,这样相比一个锁的好处是细化了锁的跨度,读写分离,减小了锁的竞争。
/** Lock held by take, poll, etc */
private final ReentrantLock takeLock = new ReentrantLock();
/** Wait queue for waiting takes */
private final Condition notEmpty = takeLock.newCondition();
/** Lock held by put, offer, etc */
private final ReentrantLock putLock = new ReentrantLock();
/** Wait queue for waiting puts */
private final Condition notFull = putLock.newCondition();
思考了下为什么ArrayBlockingQueue只用了一个锁的?我的理解是LinkedBlockingQueue是由链表组成操作的分别是头尾节点,相互竞争的关系较小。而ArrayBlockingQueue是数组,添加和删除都是在同一个数组上,虽然也可以用两个锁但是实现上需要更多的控制。