我们在并发编程中,通常需要线程安全的队列。线程安全的队列分为两种:
阻塞队列在实际应用中非常广泛,许多消息中间件中定义的队列,通常就是一种“阻塞队列”。其使用场景一般是在 “生产者-消费者” 模式中,用于线程之间的数据交换或系统解耦。“生产者-消费者”这种模式中,“生产者” 和 “消费者” 是相互独立的,两者之间的通信需要依靠一个队列。这个队列就是要说的阻塞队列。引入“阻塞队列”的最大好处就是解耦,在软件工程中,“高内聚,低耦合”是进行模块设计的准则之一,这样“生产者”和“消费者”其实是互不影响的,将来任意一方需要升级时,可以保证系统的平滑过渡。如下图:
阻塞队列BlockingQueue是一个支持两个附加操作的队列。提供了一些阻塞方法,主要作用是:
阻塞队列常用于生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程。阻塞队列是生存者用来存放元素,消费者用来获取元素的容器。
JDK提供了7种阻塞队列。如下:
本期我们学习ArrayBlockingQueue。
//数组,存储数据
final Object[] items;
//为下次 take,poll,peek,remove操作提供下标
int takeIndex;
//为下次 put, offer, or add操作提供下标
int putIndex;
//元素之和
int count;
//掌管所有访问操作的锁。全局共享。都会使用这个锁。aqs原理
final ReentrantLock lock;
//非空条件
private final Condition notEmpty;
//非满条件
private final Condition 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();
}
//创建一个指定大小,指定访问策略(非公平锁/非公平锁)并且指定给定集合元素初始化,以集合迭代器的顺序添加到阻塞队列中
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) {
checkNotNull(e);
items[i++] = e;
}
} catch (ArrayIndexOutOfBoundsException ex) {
throw new IllegalArgumentException();
}
count = i;
putIndex = (i == capacity) ? 0 : i;
} finally {
lock.unlock();
}
}
入队有四种方法,他们分别是add(E e)、offer(E e)、put(E e)、offer(E e, long timeout, TimeUnit unit),它们有什么区别呢?
//如果可以在不超过队列容量的情况下立即执行此操作,则在此队列的尾部插入指定的元素,
//如果成功,则返回true,如果队列已满则抛出IllegalStateException异常。
//如果元素为null则抛出空指针异常nullPointException
public boolean add(E e) {
return super.add(e);
}
//调用父类的add方法
public boolean add(E e) {
if (offer(e))
return true;
else
throw new IllegalStateException("Queue full");
}
//调用offer方法后面会写到
/**
* 在不超过队列容量的情况下立即执行此操作,请在此队列的尾部插入指定的元素;
* 成功返回{@code true},如果此队列已满则返回{@code false}。
* 通常,此方法比方法add 更可取,后者只能通过引发异常而无法插入元素。
*/
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 {
// 如果数组满了,使用notFull等待
// notFull等待的意思是说现在队列满了
// 只有取走一个元素后,队列才不满
// 然后唤醒notFull,然后继续现在的逻辑
// 这里之所以使用while而不是if
// 是因为有可能多个线程阻塞在lock上
// 即使唤醒了可能其它线程先一步修改了队列又变成满的了
// 这时候需要再次等待
while (count == items.length)
notFull.await();
enqueue(e);
} finally {
lock.unlock();
}
}
//如果队列满了则等待一段时间后如果队列依然满就返回false;
public boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException {
checkNotNull(e);
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length) {
if (nanos <= 0)
return false;
nanos = notFull.awaitNanos(nanos);
}
enqueue(e);
return true;
} finally {
lock.unlock();
}
}
//真正的入队操作,仅在锁定的状态下执行
private void enqueue(E x) {
final Object[] items = this.items;
//把元素直接放在指针的位置上
items[putIndex] = x;
//如果指针到数组尽头了,就返回头部
if (++putIndex == items.length)
putIndex = 0;
//计数+1
count++;
//唤醒notEmpty,因为入队了一个元素,肯定不为空。
notEmpty.signal();
}
出队也有有四个方法,它们分别是remove()、poll()、take()、poll(long timeout, TimeUnit unit);
//检索并删除此队列的头。此方法与{poll}的不同之处仅在于:如果此队列为空,它将引发异常。
public E remove() {
E x = poll();
if (x != null)
return x;
else
throw new NoSuchElementException();
}
//没有就返回null
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();
}
}
//设置指定时间段,当没有元素等待一段时间再出队,否则返回null
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0) {
if (nanos <= 0)
return null;
nanos = notEmpty.awaitNanos(nanos);
}
return dequeue();
} finally {
lock.unlock();
}
}
private E dequeue() {
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;
}
//从此队列中删除指定元素的单个实例,如果存在,则将其删除。
public boolean remove(Object o) {
if (o == null) return false;
final Object[] items = this.items;
final ReentrantLock lock = this.lock;
lock.lock();
try {
if (count > 0) {
final int putIndex = this.putIndex;
int i = takeIndex;
do {
if (o.equals(items[i])) {
removeAt(i);
return true;
}
if (++i == items.length)
i = 0;
} while (i != putIndex);
}
return false;
} finally {
lock.unlock();
}
}
void removeAt(final int removeIndex) {
final Object[] items = this.items;
if (removeIndex == takeIndex) {
// removing front item; just advance
items[takeIndex] = null;
if (++takeIndex == items.length)
takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
} else {
// an "interior" remove
// slide over all others up through putIndex.
final int putIndex = this.putIndex;
for (int i = removeIndex;;) {
int next = i + 1;
if (next == items.length)
next = 0;
if (next != putIndex) {
items[i] = items[next];
i = next;
} else {
items[i] = null;
this.putIndex = i;
break;
}
}
count--;
if (itrs != null)
itrs.removedAt(removeIndex);
}
notFull.signal();
}
其实BlockingQueue接口入队和出队的方法在实现类中思想都差不多。