ArrayBlockingQueue具有以下特性:
1)队列是有边界的,在创建时需指定队列大小;
2)队列是先进先出,从尾部进,从头部出;
3)队列已满时会阻塞添加;队列为空时会阻塞获取;
4)支持以公平和非公平的方式,写入和获取元素。
下面从源码的角度来分析下ArrayBlockingQueue的代码实现,也就能够清晰明白ArrayBlockingQueue的特性,主要从put和take过程讲解。
创建
ArrayBlockingQueue提供了3个构造方法:
// 指定容量,默认非公平
ArrayBlockingQueue(int capacity)
ArrayBlockingQueue(int capacity, boolean fair)
// 指定一个初始内容
ArrayBlockingQueue(int capacity, boolean fair, Collection<? extends E> c)
另外两个构造方法底层都是调的这个,
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
// 创建一个对象数组
this.items = new Object[capacity];
// 创建一个lock
lock = new ReentrantLock(fair);
// 用于阻塞take
notEmpty = lock.newCondition();
// 用于阻塞put
notFull = lock.newCondition();
}
ReentrantLock的加锁解锁讲解,参考文章:ReentrantLock详解
put过程
先来看一个例子,根据这个例子来看下执行过程。
public class TestBlockQueue {
public static void main(String[] args) {
final ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue(10);
final AtomicInteger atomicInteger = new AtomicInteger();
Runnable runnable = () -> {
while (true) {
try {
sleep(1000L);
int i = atomicInteger.incrementAndGet();
blockingQueue.put(i);
System.out.println("put数字 " + i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread t0 = new Thread(runnable);
Thread t1 = new Thread(runnable);
Thread t2 = new Thread(runnable);
Thread t3 = new Thread(runnable);
t0.start();
t1.start();
t2.start();
}
private static void sleep(Long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
// ignore
}
}
}
输出:
Thread-2 put数字 3
Thread-0 put数字 1
Thread-1 put数字 2
Thread-2 put数字 4
Thread-0 put数字 6
Thread-1 put数字 5
Thread-0 put数字 7
Thread-2 put数字 8
Thread-1 put数字 9
Thread-2 put数字 11
程序输出了这10个数字,就卡住了,不会继续put内容了。而且数字10,并没有输出,直接输出了11。数字10被阻塞在哪了?
先看下put代码:
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
// 锁定
lock.lockInterruptibly();
try {
while (count == items.length)
// 元素已加满,阻塞,将线程放在条件队列
notFull.await();
// 元素添加到queue
enqueue(e);
} finally {
lock.unlock();
}
}
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 加入条件队列
Node node = addConditionWaiter();
// 释放锁
int savedState = fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
// 阻塞线程
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
因此在加满元素时,线程阻塞在条件队列里,如图:
条件队列是单向的,不像ReentrantLock里的同步队列(ReentrantLock详解)是双向的。
总结下进条件队列过程:
1)元素加满了,调用notFull.await();
2)执行addConditionWaiter(),入条件队列
3)fullyRelease(node),释放当前线程占用的锁
4)线程阻塞,LockSupport.park(this)
take过程
在上面的例子里再加入线程进行获取元素:
Runnable takeRunnable = () -> {
while (true) {
try {
sleep(1000L);
blockingQueue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread t3 = new Thread(takeRunnable);
Thread t4 = new Thread(takeRunnable);
Thread t5 = new Thread(takeRunnable);
t3.start();
t4.start();
t5.start();
先看下take代码:
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
// 获取锁
lock.lockInterruptibly();
try {
while (count == 0)
// 元素为空,阻塞,将线程放在条件队列
notEmpty.await();
// 元素从queue出队
return 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条件队列发一个信号
notFull.signal();
return x;
}
我们假设t3是第一个获取到锁的线程,并且执行完notFull.signal(),还未释放锁,这个时候,ArrayBlockingQueue对象里面发生了什么?
1)取出第一个元素,takeIndex+1
2)执行notFull.signal(),将条件队列的第一个出列,并加入到lock的同步队列
3)如果t4、t5线程已经开始执行,并在等待t3释放锁
4)当t3释放锁,因为t4、t5会取数,同样会把t0、t2,加入到同步队列
5)当items没有内容时,会执行notEmpty.await(),阻塞取数线程。notEmpty也是一个条件队列,会存储取数的线程(t3、t4、t5),跟notFull类似。
总结
1)定义一个数组items,存储内容;
2)使用ReentrantLock,控制安全的存储和读取,并可实现公平和非公平;
3)使用两个条件队列notFull和notEmpty,来控制数组满和空的情况下,阻塞线程;
4)put或take快结束时,使用notEmpty和notFull的signal方法,把阻塞在条件队列的node转移到同步队列,以便可以继续读取或写入数据。