在之前分析线程池ThreadExecutorPool的时候,我们就有接触到阻塞队列这一个概念。所谓阻塞队列,就是一个在传统队列基础上,支持两个附加操作的队列。而这两个附加的操作支持阻塞的插入和移除方法。
说完关于阻塞队列的一些概念,我们就直接切入正题,JUC包提供了多种阻塞队列的具体实现类,而我们就以其中的ArrayBlockingQueue(一个由数组结构组成的有界阻塞队列)类进行分析,来了解阻塞队列的实现机制。
我们可以看到,ArrayBlockingQueue继承了AbstractQueue,说明ArrayBlockingQueue具备普通队列的基本操作,在基本队列的基础上,ArrayBlockingQueue还实现了BlockingQueue接口。
我们来看一下BlockingQueue接口的实现。
public interface BlockingQueue<E> extends Queue<E> {
/*将元素e插入阻塞队列中,若超过队列容量限制,那么会抛出异常*/
boolean add(E e);
/**
* 将元素e插入阻塞队列中,若超过队列容量限制,那么会返回fasle
* 当使用有界阻塞队列时,offer()方法优于add()
*/
boolean offer(E e);
/*将元素e插入阻塞队列中,若超过队列容量限制,那么当前线程会被阻塞,直至队列不满/被中断*/
void put(E e) throws InterruptedException;
/*将元素e插入阻塞队列中,若超过队列容量限制,那么当前线程会被阻塞,直至队列不满/被中断/超时退出*/
boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException;
/*移除队列的首部,若队列为空,那么当前线程会被阻塞,直至队列不为空/被中断*/
E take() throws InterruptedException;
/*移除队列的首部,若队列为空,那么当前线程会被阻塞,直至队列不为空/被中断/超时退出*/
E poll(long timeout, TimeUnit unit)
throws InterruptedException;
/*队列的剩余容量*/
int remainingCapacity();
/*从队列中移除某个指定元素*/
boolean remove(Object o);
/*判断队列中是否包含某个执行元素*/
public boolean contains(Object o);
/*移除此队列中所有可用的元素,并将它们添加到给定collection 中。*/
int drainTo(Collection<? super E> c);
/*最多从此队列中移除给定数量的可用元素,并将这些元素添加到给定collection中。*/
int drainTo(Collection<? super E> c, int maxElements);
}
以上便是BlockingQueue的接口实现,接下来我们来正式分析ArrayBlockingQueue。
/*队列的底层存储元素的结构,一个Object数组*/
final Object[] items;
/*下一次执行take,poll,peek或者remove操作的元素的位置*/
int takeIndex;
/*下一次执行put、offer或者add操作的元素的位置*/
int putIndex;
/*队列中的元素数量*/
int count;
/*队列的主锁*/
final ReentrantLock lock;
/*若队列为空,那么在执行获取元素的线程将在该Condition队列上等待*/
private final Condition notEmpty;
/*若队列满了,那么执行插入操作的线程将在该Condition队列上等待*/
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();
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();
}
}
该构造函数可以指定队列的大小,并且指定队列使用的锁的类型。还可以将指定集合的元素全部插入队列中。
首先来看一下阻塞队列的插入函数。
boolean offer(E e):
public boolean offer(E e) {
/*检查待插入元素是否为null*/
checkNotNull(e);
/*加锁*/
final ReentrantLock lock = this.lock;
lock.lock();
try {
/*如果队列中元素的数量超过容量限制*/
if (count == items.length)
return false;
else {
/*插入元素*/
enqueue(e);
return true;
}
} finally {
lock.unlock();
}
}
offer的执行流程:
我们来看一下具体的插入过程:enqueue(e)
private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
/*获取底层数组*/
final Object[] items = this.items;
/*将元素x插入数组*/
items[putIndex] = x;
/*调整putIndex,记住,该队列是一个环形队列*/
if (++putIndex == items.length)
putIndex = 0;
/*队列元素数量+1*/
count++;
/*唤醒等待获取元素的线程,只唤醒一个线程*/
notEmpty.signal();
}
将有元素插入队列中,执行完插入操作之后,唤醒等待获取元素的线程(Condition队列[notEmpty]中的第一个结点)去尝试获取元素。
add(E e):抛出异常的插入操作
public boolean add(E e) {
return super.add(e);
}
public boolean add(E e) {
if (offer(e))
return true;
else
throw new IllegalStateException("Queue full");
}
可以看到,add其实是调用offer函数,只是add操作会根据offer操作的返回值来确定是否抛出异常。
put(E e): 阻塞的插入操作
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
/*加锁,对中断敏感,会响应中断*/
lock.lockInterruptibly();
try {
/*如果队列的元素数量已经超过容量限制,将调用该方法的线程进行阻塞*/
while (count == items.length)
notFull.await();
enqueue(e);
} finally {
lock.unlock();
}
}
put操作其实就是在offer操作的基础上,多了阻塞当前线程的插入操作的步骤,当队列满了,会将当前线程加入到notFull这个Condition队列中。
offer(E e, long timeout, TimeUnit unit)
在这里不再赘述,只是多了个超时退出的环节。
讲完队列的插入操作,接着我们来了解一下阻塞队列获取元素的操作。
E take():
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;
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex];
/*在更新takeIndex之前把原先的位置设置为null,避免无意识的对象持有导致内存泄露*/
items[takeIndex] = null; //help gc
/*更新takeIndex*/
if (++takeIndex == items.length)
takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
/*唤醒等待插入元素的线程*/
notFull.signal();
return x;
}
dequeue()
的流程也很简单,不再赘述。我们注意一下items[takeIndex] = null
操作,当我们将元素从队列中弹出时需要将原先的位置设置为null,避免无意识的对象持有导致内存泄露。
获取元素操作的阻塞版本和定时版本和插入元素的相似。
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
/*直接返回,不会被阻塞*/
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return (count == 0) ? null : dequeue();
} finally {
lock.unlock();
}
}
/*定时等待版本*/
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) {
/*若超时,则直接返回null*/
if (nanos <= 0)
return null;
/*当前线程在Condition队列上超时等待*/
nanos = notEmpty.awaitNanos(nanos);
}
return dequeue();
} finally {
lock.unlock();
}
}
执行take
操作的线程会一直阻塞在notEmpty这个Condition队列上,等待其他线程将它唤醒。
而执行poll(long timeout, TimeUnit unit)
操作的线程被阻塞一段时间,如果这段时间内未获取元素,那么会直接返回null。
接下来我们看一下remove
函数的实现:
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();
}
}
remove操作
会遍历takeIndex到putIndex之间(环形结构)的元素并一一进行比较,若匹配,会调用removeAt(i)
执行真正的移除操作。
void removeAt(final int removeIndex) {
final Object[] items = this.items;
/*如果移除的元素目标刚好为takeIndex,那么就不需要进行队列的调整*/
if (removeIndex == takeIndex) {
/*直接将takeIndex位置上的对象置为null*/
items[takeIndex] = null;
/*更新takeIndex*/
if (++takeIndex == items.length)
takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
} else {
/*将removeIndex和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();
}
阻塞队列的其他API在这里就不再分析了,我们已经大概了解阻塞队列是如何实现支持阻塞的插入和获取操作。
他通过了ReentrantLock和Condition,使用了 “生产者-消费者” 的多线程设计模式来实现了这两种操作。
至此,阻塞队列ArrayBlockingQueue
的分析已经完毕。
我们利用阻塞队列来实现一个生产者-消费者的例子:
产品:
@ToString
@Data
@AllArgsConstructor
public class Product {
private String name;
}
生产者:
public class Producer implements Runnable{
private static AtomicInteger count = new AtomicInteger(0);
private static volatile boolean isRunning = true;
private BlockingQueue<Product> queue;
private static final long EXECUTION_TIME = 1000;
private static Random random = new Random();
public Producer(BlockingQueue<Product> queue) {
this.queue = queue;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "线程准备开始生产产品");
while (isRunning){
try {
TimeUnit.MILLISECONDS.sleep(random.nextInt((int)EXECUTION_TIME) + EXECUTION_TIME);
String productName = "产品" + count.getAndIncrement();
Product product = new Product(productName);
if(!queue.offer(product)){
System.out.println("产品已经满了!" + Thread.currentThread().getName() + "准备阻塞");
queue.put(product);
}
System.out.println(product + "生产完毕");
} catch (InterruptedException e) {
System.out.println("当前线程被中断");
Thread.currentThread().interrupt();
}
}
}
public void stop(){
isRunning = false;
}
}
消费者:
public class Consumer implements Runnable{
private static volatile boolean isRunning = true;
private final BlockingQueue<Product> queue;
private static final long EXECUTION_TIME = 1000;
private static Random random = new Random();
public Consumer(BlockingQueue<Product> queue) {
this.queue = queue;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "准备开始消费产品");
while (isRunning){
try {
TimeUnit.MILLISECONDS.sleep(EXECUTION_TIME);
Product product = null;
if((product = queue.poll()) == null){
System.out.println("产品为空," + Thread.currentThread().getName() + "准备阻塞");
product = queue.take();
}
System.out.println(product + "被消费");
} catch (InterruptedException e) {
System.out.println("线程被中断");
Thread.currentThread().interrupt();
}
}
}
public void stop(){
isRunning = false;
}
}
主函数:
public class Main {
static class CThreadFactory implements ThreadFactory{
private AtomicInteger count = new AtomicInteger(1);
@Override
public Thread newThread(Runnable runnable) {
return new Thread(runnable, "消费者线程" + count.getAndIncrement());
}
}
static class PThreadFactory implements ThreadFactory{
private AtomicInteger count = new AtomicInteger(1);
@Override
public Thread newThread(Runnable runnable) {
return new Thread(runnable, "生产者线程" + count.getAndIncrement());
}
}
private static final int CONSUMER_NUM = 5;
private static final int PRODUCER_NUM = 10;
private static ExecutorService consumerService = Executors.newFixedThreadPool(CONSUMER_NUM, new CThreadFactory());
private static ExecutorService producerService = Executors.newFixedThreadPool(PRODUCER_NUM, new PThreadFactory());
public static void main(String[] args) {
try {
ArrayBlockingQueue<Product> queue = new ArrayBlockingQueue<>(10);
for (int i = 0; i < PRODUCER_NUM; i++) {
producerService.execute(new Producer(queue));
}
for (int i = 0; i < CONSUMER_NUM; i++) {
consumerService.execute(new Consumer(queue));
}
}finally {
consumerService.shutdown();
producerService.shutdown();
}
}
}
以上便是我们利用阻塞队列来实现的消费者-生产者。我们还可以使用java内部机制notify/wait或者JUC包的await/signal。这里便不再多说。