先看类图
ArrayBlockingQueue的内部有一个数组items,用来存放队列元素,putindex变量表示入队元素下标,takeIndex是出队下标,count统计队列元素个数。从定义可知,这些变量都没有使用volatile修饰,这是因为访问这些变量都是在锁块内,而加锁就已经保证了锁块内变量的内存可见性了。另外还有个独占锁lock用来保证出、入队操作的原子性,这保证了同时只有一个线程可以进行入队、出队操作。另外,notEmpty、notFull条件变量用来进行出、入队同步。
public ArrayBlockingQueue(int capacity) {
this(capacity, 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();
}
由于ArrayBlockingQueue是有界队列,所以构造函数必须传入队列大小参数。
offer操作
向队列尾部插入一个元素,如果队列有空闲则插入成功后返回true,如果队列已满则丢弃当前元素然后返回false。另外,该方法是不可阻塞的。
public boolean offer(E e) {
checkNotNull(e);
// 获取独占锁,当前线程获取该锁后,其它入队和出队操作的线程都会被阻塞后放入lock锁的AQS阻塞队列
final ReentrantLock lock = this.lock;
lock.lock();
try {
if (count == items.length)
return false;
else {
enqueue(e);
return true;
}
} finally {
lock.unlock();
}
}
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();
}
如上首先把当前元素放入items数组,然后计算下一个元素应该存放的下标位置,并递增元素个数计数器,最后激活notEmpty的条件队列中因为调用take操作而被阻塞的一个线程。这里由于在操作共享变量count前加了锁,所以不存在内存不可见问题,加过锁后获取的共享变量都是从主内存获取的,而不是从CPU缓存或者寄存器获取。
put操作
向队列尾部插入一个元素,如果队列有空闲则插入后直接返回true,如果队列已满则阻塞当前线程知道队列有空闲并插入成功后返回true,如果在阻塞时被其他线程设置了中断标志,则被阻塞线程会抛出InterruptedException异常。
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
// 如果队列满,则把当前线程放入notFull管理的条件队列
while (count == items.length)
notFull.await();
enqueue(e);
} finally {
lock.unlock();
}
}
poll操作
从队列头部获取并移除一个元素,如果队列为空则返回null,该方法是不阻塞的
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return (count == 0) ? null : dequeue();
} finally {
lock.unlock();
}
}
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;
}
详细说一下dequeue方法,首先获取当前队头元素并将其保存到局部变量,然后重置队头元素为null,并重新设置队头下标,递减元素计数器,最后发送信号激活notFull的条件队列里面一个因为调用put方法而被阻塞的线程。
take操作
相比poll,如果队列为空则阻塞当前线程直到队列不为空然后返回元素,如果在阻塞时被其它线程设置了中断标志,则被阻塞线程会抛出异常。
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
// 如果队列为空则把当前线程挂起后放入notEmpty的条件队列,等待其它线程调用notEmpty.await()方法。
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
peek操作与size操作比较简单,前者是获取锁后然后从数组中获取当前队头下标的值并返回,然后释放锁;后者是获取锁后直接放回count,并在返回前释放锁
小结
如图所示,ArrayBlockingQueue通过使用全局独占锁实现了同时只能有一个线程进行入队或者出队操作,这个锁的粒度比较大,有点类似于在方法上添加synchronized的意思。其中offer和poll操作通过简单的加锁进行入队、出队操作,而put、take操作则使用条件变量实现了,如果队列满则等待,如果队列空则等待,然后分别在出队和入队操作中发送信号激活等待线程实现同步。另外,相比LinkedBlockingQueue,ArrayBlockingQueue的size操作是精确的,因为计算前加了全局锁。