java阻塞队列---BlockingQueue
java阻塞队列—BlockingQueue
阻塞队列解决的问题
-
为什么要用阻塞队列?
在日常编码中, 我们会使用到生产者消费者模式, 使用阻塞队列可以快速的实现这一模式
-
线程池中阻塞队列的使用
在ThreadPoolExecutor中, 当核心线程数给占用完毕后, 后来的任务都会被加入到阻塞队列中.
这里为什么不用普通队列?
首先, 线程池的需求是核心线程会一直保持运行, 而worker在执行完毕当前任务之后, 会从队列中获取下一个任务来执行, 如果队列中没有任务, worker需要等待任务的到来并且获取他
所以阻塞队列的特性完美契合了这一需求
如果我们使用普通队列的话, 线程的阻塞和唤醒需要我们额外的编写代码来实现, 这无疑增加了并发编程的复杂性
ArrayBlockingQueue
在juc包中有很多类都实现了BlockingQueue接口, 这里我们主要介绍一下ArrayBlockingQueue
与ArrayList不同的是, ArrayBlockingQueue没有无参构造器, 我们必须给该队列确定一个容量, 所以该队列是无法自动扩容的
offer()
阻塞队列也是实现了队列接口的, 所以offer方法在这里也会被实现
public boolean offer(E e) {
checkNotNull(e);
final ReentrantLock lock = this.lock;
// 获取资源锁, 入队和出队操作都需要获取这个锁
// 所以可以得知 入队和出队操作在该类中是无法同时进行的
lock.lock();
try {
// 队列如果是满的, 则直接拒绝
// offer中这里是不阻塞的
if (count == items.length)
return false;
else {
// 队列没满则进行入队操作
enqueue(e);
return true;
}
} finally {
lock.unlock();
}
}
// 带有入参的offer方法, 在队列满的时候, 该方法会根据入参来设置等待时间
// 在时间内如果队列有消费行为, 那么需要入列的数据则有机会入列(并发情况)
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();
}
}
enqueue()
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随机唤醒一个await的线程
// 因为有数据入队, 所以队列中一定不为空, 那么消费者可以消费
notEmpty.signal();
}
take()
从阻塞队列中获取数据, 如果没有数据则阻塞等待获取, 这里的阻塞不会被时间打破, 只有在线程中断的时候会才会停止阻塞
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
// 队列为空, 阻塞等待唤醒
// 入队的时候notEmpty信号量会被signal
while (count == 0)
notEmpty.await();
// 出队操作
return dequeue();
} finally {
lock.unlock();
}
}
dequeue()
在dequeue中并没有判断队列是否为空, 因为在进入这个方法之前, 都会同步判断count是否为0
只有count不为0的时候才会进入这个方法
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;
// 移动指针, 循环队列指针到达队尾+1则变为0
if (++takeIndex == items.length)
takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
// 消费完毕之后队列非满, 唤醒一个生产者生产数据
notFull.signal();
return x;
}
总结
在ArrayBlockingQueue中, 所有的入队出队操作都是在上锁之后进行的, 所以无需进行并发的操作处理, 同一时间只有一个线程进行入队出队操作
总的来说就是一个数组实现的队列, 加上信号量实现生产者消费者模式, 但是入队出队操作只有一个线程可以进行, 有点浪费性能就是了