上一篇文章Android之阻塞队列LinkedBlockingQueue使用及源码解析讲到了LinkedBlockingQueue,与之相关的一个队列就是今天要讲到的ArrayBlockingQueue。
ArrayBlockingQueue也是一个阻塞队列,内部使用一个定长数组保存数据,在构造这个队列的时候必须要指定长度,如果队列已满再执行入队将会阻塞;如果队列为空再执行出队将会阻塞。
看看源码是怎么构建这个类的:
/** 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;
/**
* Shared state for currently active iterators, or null if there
* are known not to be any. Allows queue operations to update
* iterator state.
* 当前迭代器状态
*/
transient Itrs itrs = null;
可以看出来定义了一个数组去维护数据,而不是像LinkedBlockingQueue用链表维护数据;然后定义了一个锁和出队入队的判断条件,同样跟链表不同,链表定义了两把锁分别用于管理出队和入队,从这里也可以看出来,在高并发的情况下,链表队列可能会比数组队列更好用,效率更高。
再看构造方法
/**
* Creates an {@code ArrayBlockingQueue} with the given (fixed)
* capacity and default access policy.
*
* @param capacity the capacity of this queue
* @throws IllegalArgumentException if {@code capacity < 1}
*
* 指定数组大小
*/
public ArrayBlockingQueue(int capacity) {
this(capacity, false);
}
/**
* Creates an {@code ArrayBlockingQueue} with the given (fixed)
* capacity and the specified access policy.
*
* @param capacity the capacity of this queue
* @param fair if {@code true} then queue accesses for threads blocked
* on insertion or removal, are processed in FIFO order;
* if {@code false} the access order is unspecified.
* @throws IllegalArgumentException if {@code capacity < 1}
*
* 指定数组大小,
* fair 为true 先来的线程先操作 为false 等待的线程按随机顺序操作
*/
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
/**
* Creates an {@code ArrayBlockingQueue} with the given (fixed)
* capacity, the specified access policy and initially containing the
* elements of the given collection,
* added in traversal order of the collection's iterator.
*
* @param capacity the capacity of this queue
* @param fair if {@code true} then queue accesses for threads blocked
* on insertion or removal, are processed in FIFO order;
* if {@code false} the access order is unspecified.
* @param c the collection of elements to initially contain
* @throws IllegalArgumentException if {@code capacity} is less than
* {@code c.size()}, or less than 1.
* @throws NullPointerException if the specified collection or any
* of its elements are null
*
* 多加了一个集合
*/
public ArrayBlockingQueue(int capacity, boolean fair,
Collection<? extends E> c) {
this(capacity, fair);
final ReentrantLock lock = this.lock;
lock.lock(); // Lock only for visibility, not mutual exclusion
try {
//讲集合数据添加到数组中
int i = 0;
try {
for (E e : c) {
if (e == null) throw new NullPointerException();
items[i++] = e;
}
} catch (ArrayIndexOutOfBoundsException ex) {
throw new IllegalArgumentException();
}
//数组元素个数
count = i;
//下一次入队的索引
putIndex = (i == capacity) ? 0 : i;
} finally {
lock.unlock();
}
}
构造方法里会对数组初始化,并且通过fair参数来构造一个公平锁或者不公平锁。
接下来看看入队方法
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
//数组如果满了,那就阻塞在这里
while (count == items.length)
notFull.await();//一直等待条件notFull,即被其他线程唤醒,就是有线程将一个元素出队了,然后调用notFull.signal()唤醒其他等待这个条件的线程
//将元素入队
enqueue(e);
} finally {
lock.unlock();
}
}
private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
final Object[] items = this.items;
//如果调用第一个或者第二个构造方法,那putIndex就是0
items[putIndex] = x;
//如果下一次入队索引等于数组长度,就把索引置为0,下次就从数组头部开始插入元素,循环数组
if (++putIndex == items.length) putIndex = 0;
count++;
//唤醒因为数组为空而阻塞的线程
notEmpty.signal();
}
入队逻辑其实挺简单的,也没啥好说的
再来看看出队方法
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
//加锁
lock.lockInterruptibly();
try {
//如果数组里没有元素,就阻塞
while (count == 0)
notEmpty.await();
//取出元素并返回
return dequeue();
} finally {
//释放锁
lock.unlock();
}
}
private E dequeue() {
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
final Object[] items = this.items;
//如果一次都没出队,那takeIndex = 0
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex];
items[takeIndex] = null;//取出该索引下的元素然后将数组该索引下的置为null,方便回收
//如果取完数据后数组为null,那下次从0索引出取数据,就这样循环
if (++takeIndex == items.length) takeIndex = 0;
//数组数量减一
count--;
if (itrs != null)
itrs.elementDequeued();
//唤醒因为数组已满而等待入队的线程
notFull.signal();
return x;
}
总结以上出队入队的逻辑就是:
调用put方法入队,如果数组已满,那就调用notFull.await(),这个A线程就会放弃锁进入阻塞状态;如果这时候有B线程调用take方法取走了一个数据,调用notFull.signal()唤醒等待的线程,这时候A线程就被唤醒获取锁,调用enqueue()方法把数据插入到数组。
调用take方法出队,如果数组为空,那就调用notEmpty.await(),这个C线程就会放弃锁进入阻塞状态,如果这时候有D线程调用put方法添加了一个数据,调用了notEmpty.signal();唤醒等待线程,这时候C线程就被唤醒获取锁,调用dequeue()方法把数据从数组取出。
其它出队入队方法与LinkedBlockingQueue类的方法类似。
现在用图说明出队入队逻辑:
1.假设数组大小是3,然后入队了三个元素
2.接下来开始出队,出队索引默认是0,这个位置值变为null
3.接下来继续出队,这时候索引是1,就将数组item1的值变为null
4.接下来开始入队,由于第一步入队操作,添加了三个元素,跟数组大小一样,入队索引变为0
这里我们就可以比较下ArrayBlockingQueue(简称abq)和LinkedBlockingQueue(简称lbq):
1.abq使用一把锁来对出队入队操作进行控制,表面同一时刻只能进行出队操作或者入队操作;lbq使用两把锁分别控制出队和入队,说明同一时刻可以同时进行出队和入队操作;显然lbq中在高并发的时候阻塞情况会比abq少,性能会优于abq。
2.两者在出队入队操作时都是用锁,所以都是线程安全的实现。
3.abq使用的是一个定长数组,且在出队入队的时候是循环操作数组,看上面代码注释可知,相比于lbq来说内存利用率方面会占优势;不过abq在初始化的时候就会提前给数组分配内存,而lbq是使用多少就分配多少内存,从这点来看lbq在内存方面又小占优势。
这个类的使用跟上一篇文章的LinkedBlockingQueue使用几乎一样,只不过abq不同同时进行添加元素和删除元素操作,而lbq可以。