1. 阻塞队列介绍
1.1 队列
- 是限定在一端进行插入,另一端进行删除的特殊的线性表
- 先进先出线性表
- 允许出队的一端称为队头,允许入队的一段称为队尾 有基于数组的队列、基于链表的队列
Queue接口
public interface Queue<E> extends Collection<E> {
//添加一个元素,添加成功返回true, 如果队列满了,就会抛出异常
boolean add(E e);
//添加一个元素,添加成功返回true, 如果队列满了,返回false boolean offer(E e);
//返回并删除队首元素,队列为空则抛出异常
E remove();
//返回并删除队首元素,队列为空则返回
null E poll();
//返回队首元素,但不移除,队列为空则抛出异常
E element();
//获取队首元素,但不移除,队列为空则返回
null E peek(); }
1.2 阻塞队列
队列满了生产者线程阻塞,队列空了,把消费者线程阻塞
阻塞队列 (BlockingQueue)是Java util.concurrent包下重要的数据结构,BlockingQueue提供了线程 安全的队列访问方式:当阻塞队列插入数据时,如果队列已满,线程将会阻塞等待直到队列非满;从阻塞队列取数据时,如果队列已空,线程将会阻塞等待直到队列非空。并发包下很多高级同步类的实 现都是基于BlockingQueue实现的。
BlockingQueue接口
在Queue接口中方法的基础上新增了入队和出队的方法:
- 入队:put(e)
- 出队:take()
应用场景
阻塞队列在实际应用中有很多应用场景,以下是常见的应用场景。
线程池
线程池中的任务队列通常是一个阻塞队列。当任务数超过线程池的容量时,新提交的任务将被放入任 务队列中等待执行。线程池中的工作线程从任务队列中取出任务进行处理,如果队列为空,则工作线 程会被阻塞,直到队列中有新的任务被提交。
生产者-消费者模型
在生产者-消费者模型中,生产者向队列中添加元素,消费者从队列中取出元素进行处理。阻塞队列可 以很好地解决生产者和消费者之间的并发问题,避免线程间的竞争和冲突。
消息队列
- 消息队列使用阻塞队列来存储消息,生产者将消息放入队列中,消费者从队列中取出消息进行处理。
- 消息队列可以实现异步通信,提高系统的吞吐量和响应性能,同时还可以将不同的组件解耦,提高系 统的可维护性和可扩展性。
缓存系统
缓存系统使用阻塞队列来存储缓存数据,当缓存数据被更新时,它会被放入队列中,其他线程可以从 队列中取出最新的数据进行使用。使用阻塞队列可以避免并发更新缓存数据时的竞争和冲突。
并发任务处理
在并发任务处理中,可以将待处理的任务放入阻塞队列中,多个工作线程可以从队列中取出任务进行 处理。使用阻塞队列可以避免多个线程同时处理同一个任务的问题,并且可以将任务的提交和执行解 耦,提高系统的可维护性和可扩展性。 总之,阻塞队列在实际应用中有很多场景,它可以帮助我们解决并发问题,提高程序的性能和可靠 性。
JUC包下面的阻塞队列
BlockingQueue 接口的实现类都被放在了 juc 包中,它们的区别主要体现在存储结构上或对元素操作 上的不同,但是对于take与put操作的原理却是类似的。
ArrayBlockingQueue
其内部使用数组存储元素的,初始化时侯需要指定容量大下,利用ReentrantLock保证线程安全。ArraryBlockingQueue是典型的有界阻塞队列。主要用于场景:
- 实现数据缓存
- 限流
- 生产者-消费者模式 、、、、、、、
ArraryBlockingQueue的使用
BlockingQueue queue = new ArraryBlockingQueue(1024);
queue.put("1");//向队列中添加元素
Object object = queue.take();//从队列中取出元素
ArraryBlockingQueue的原理
ArrayBlockingQueue使用独占锁ReentrantLock实现线程安全,入队和出队操作使用同一个锁对象, 也就是只能有一个线程可以进行入队或者出队操作;这也就意味着生产者和消费者无法并行操作,在 高并发场景下会成为性能瓶颈。
数据结构
使用Lock锁的Condition通知机制进行阻塞控制 核心:一把锁,两个条件。高并发下性能比较慢。
//数据元素数组
final Object[] items;
//下一个待取出元素索引
int takeIndex;
//下一个待添加元素索引
int putIndex;
//元素个数
int count;
//内部锁
final ReentrantLock lock;
//消费者
private final Condition notEmpty;
//生产者
private final Condition notFull;
public ArrayBlockingQueue(int capacity) {
this(capacity, false);
}
public ArrayBlockingQueue(int capacity, boolean fair) {
lock = new ReentrantLock(fair); //公平,非公平
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
入队put
public void put(E e) throws InterruptedException{
//检查是否为空
checkNotNull(e);
final ReentrantLock lock = this.lock;
//加锁,如果线程中断抛出异常
lock.lockInterruptibly();
try {
//阻塞队列已满,则将生产者挂起,等待消费者唤醒
//设计注意点: 用while不用if是为了防止虚假唤醒
while (count == items.length)
notFull.await();
//队列满了,使用notFull等待(生产者阻塞)
// 入队
enqueue(e);
} finally {
lock.unlock();
// 唤醒消费者线程 }
}
private void enqueue(E x) {
final Object[] items = this.items;
//入队 使用的putIndex
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0; //设计的精髓: 环形数组,putIndex指针到数组尽头了,返回头部
count++; //notEmpty条件队列转同步队列,准备唤醒消费者线程,因为入队了一个元素,肯定不为空了
notEmpty.signal();
}
}
为什么ArraryListBlockingQueue对数组操作要设置成双指针?
使用双指针的好处在于可以避免数组的复制操作。如果使用单指针,每次删除元素时需要将后面的元 素全部向前移动,这样会导致时间复杂度为 O(n)。而使用双指针,我们可以直接将 takeIndex 指向下 一个元素,而不需要将其前面的元素全部向前移动。同样地,插入新的元素时,我们可以直接将新元 素插入到 putIndex 所指向的位置,而不需要将其后面的元素全部向后移动。这样可以使得插入和删除 的时间复杂度都是 O(1) 级别,提高了队列的性能。
出队take
```java
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() {
final Object[] items = this.items;
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex]; //取出takeIndex位置的元素
items[takeIndex] = null;
if (++takeIndex == items.length)
takeIndex = 0; //设计的精髓: 环形数组,takeIndex 指针到数组尽头了,返回头部 count--;
if (itrs != null)
itrs.elementDequeued();
//notFull条件队列转同步队列,准备唤醒生产者线程,此时队列有空位
notFull.signal();
return x;
}