(一)基础部分
1、先看看阻塞队列的接口架构图
通过接口架构图可知,阻塞队列BlockingQueue的父类Queue(队列)和List集合与Set集合并列存在。而BlockingQueue有两个兄弟类,Deque(双管队列)、AbstractQueue(非阻塞队列)
2、什么是阻塞队列?
阻塞队列,BlockingQueue(接口),是在队列(Queue)的基础上支持了两个附加操作的队列。
2个附加操作:
支持阻塞的插入方法:队列满时,队列会阻塞插入元素的线程,直到队列不满。
支持阻塞的移除方法:队列空时,获取元素的线程会等待队列变为非空。
3、阻塞队列的应用场景
阻塞队列常用于生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程。简而言之,阻塞队列是生产者用来存放元素、消费者获取元素的容器。
4、阻塞的队列的分类(七种)
1)ArrayBlockingQueue 数组结构组成的有界阻塞队列。
此队列按照先进先出(FIFO)的原则对元素进行排序,但是默认情况下不保证线程公平的访问队列,即如果队列满了,那么被阻塞在外面的线程对队列访问的顺序是不能保证线程公平(即先阻塞,先插入)的。
2)LinkedBlockingQueue一个由链表结构组成的有界阻塞队列
此队列按照先出先进的原则对元素进行排序
3)PriorityBlockingQueue 支持优先级的无界阻塞队列
4)DelayQueue 支持延时获取元素的无界阻塞队列,即可以指定多久才能从队列中获取当前元素
5)SynchronousQueue不存储元素的阻塞队列,每一个put必须等待一个take操作,否则不能继续添加元素。并且他支持公平访问队列。
6)LinkedTransferQueue由链表结构组成的无界阻塞TransferQueue队列。相对于其他阻塞队列,多了tryTransfer和transfer方法
transfer方法
如果当前有消费者正在等待接收元素(take或者待时间限制的poll方法),transfer可以把生产者传入的元素立刻传给消费者。如果没有消费者等待接收元素,则将元素放在队列的tail节点,并等到该元素被消费者消费了才返回。
tryTransfer方法
用来试探生产者传入的元素能否直接传给消费者。,如果没有消费者在等待,则返回false。和上述方法的区别是该方法无论消费者是否接收,方法立即返回。而transfer方法是必须等到消费者消费了才返回。
7)LinkedBlockingDeque链表结构的双向阻塞队列,优势在于多线程入队时,减少一半的竞争。
5、什么叫做阻塞队列的有界和无界
阻塞队列有一个非常重要的属性,那就是容量的大小,分为有界和无界两种。
无界队列意味着里面可以容纳非常多的元素,例如 LinkedBlockingQueue 的上限是 Integer.MAX_VALUE,约为 2 的 31 次方,是非常大的一个数,可以近似认为是无限容量,因为我们几乎无法把这个容量装满。但是有的阻塞队列是有界的,例如 ArrayBlockingQueue 如果容量满了,也不会扩容,所以一旦满了就无法再往里放数据了。
6、公平锁
比如说现在队列是满的,还有很多线程执行 put 操作,必然会有很多线程阻塞等待,当有其它线程执行 take 时,会唤醒等待的线程,如果是公平锁,会按照阻塞等待的先后顺序,依次唤醒阻塞的线程,如果是非公平锁,会随机唤醒沉睡的线程
(二)阻塞队列的四种处理方式
1、抛出异常
- 添加:add
- 删除:remove
public class test04 {
public static void main(String[] args) {
BlockingQueue blockingQueue=new ArrayBlockingQueue(3);//参数为队列的大小
//add() --队列添加元素
System.out.println(blockingQueue.add("a"));
System.out.println(blockingQueue.add("b"));
System.out.println(blockingQueue.add("c"));
//队列满了,在添加报错
System.out.println(blockingQueue.add("d"));
System.out.println("***********************");
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
//队列空,报异常
System.out.println(blockingQueue.remove());
}
}
队列满再次添加元素,会报异常,如下图:
为什么会报异常?
查看add的部分源码
public boolean add(E e) {
//调用offer传入要添加的元素e
if (offer(e))
//添加成功返回true
return true;
else
//抛出异常Queue full
throw new IllegalStateException("Queue full");
}
查看源码可知,add方法的底层调用了offer方法,添加元素成功返回true,失败抛出异常
2、不抛出异常,返回特殊值
- 添加:offer
- 移除:pol
l
public static void test02(){
BlockingQueue blockingQueue=new ArrayBlockingQueue(3);//参数为队列的大小
//offer() --队列添加元素
System.out.println(blockingQueue.offer("a"));
System.out.println(blockingQueue.offer("b"));
System.out.println(blockingQueue.offer("c"));
//队列满了,再次添加为false,不会报异常
System.out.println(blockingQueue.offer("d"));
//poll()--移除队列中的元素
System.out.println("***********************");
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
//队列空,再次移除为null
// System.out.println(blockingQueue.poll());
}
运行结果:添加元素,超过队列大小会返回一个false
运行结果:移除元素,当队列中元素为null时,再次移除会返回一个null
3、一直阻塞
- 添加:put
- 移除:take
public static void test03() throws InterruptedException {
BlockingQueue blockingQueue=new ArrayBlockingQueue(3);//参数为队列的大小
//offer() --队列添加元素
blockingQueue.put("a");
blockingQueue.put("b");
blockingQueue.put("c");
blockingQueue.put("d");
//队列满了,再次添加将进入等待状态
System.out.println(blockingQueue.offer("d"));
//poll()--移除队列中的元素
System.out.println("***********************");
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
//队列空,再次移除将进入等待状态
System.out.println(blockingQueue.take());
}
运行结果:添加元素,超过队列大小将进入等待状态,程序不停止,一直等待,直到队列中有元素被移除
思考:为什么会一直阻塞?
(看完下面的源码分析就会有结果了)
运行结果:移除元素,队列元素为空时,将进入等待状态,程序不停止,直到队列中有元素添加进来
4、超时退出
-
添加操作:offer(e,time,unit),是offer(e)方法的一个构造方法,新增了参数time。意为当添加元素,队列满时,再次添加,将进入等待状态,等待时长为参数给定时长,如果超过给定时长,还没有元素移除队列中,就自动退出
-
移除操作:poll(time.unit),是poll()方法的一个构造方法,新增参数time。当队列为空时,再次移除,将进入等待状态,等待时长为参数给定时长,超过时长将退出
(三)源码分析-ArrayBlockingQueue
1、put和take操作
队尾插入元素(put),队头拿出元素(take)
2、特点
- 有界数组,容量创建,不可修改
- 元素有序,先进先出
- 队列满或空时,put和take会被阻塞
3、源码分析
初始化定义,数据结构部分
// 初始化数组
final Object[] items;
// 拿数据的索引位置
int takeIndex;
// 放数据的索引位置
int putIndex;
// 当前队列元素个数
int count;
// 可重入的锁
final ReentrantLock lock;
// 移除元素的队列
private final Condition notEmpty;
// 添加元素的队列
private final Condition notFull;
构造方法
有两个参数:队列大小,是否公平
public ArrayBlockingQueue(int capacity, boolean fair) {
//队列大小小于等于0
if (capacity <= 0)
//抛出异常
throw new IllegalArgumentException();
//new一个object类型的数组,数组大小为队列大小,并赋值给初始化数组
this.items = new Object[capacity];
//定义一个可重入锁,传入的参数是否公平
lock = new ReentrantLock(fair);
//队列非空,创建Condition,在执行put操作成功后使用
notEmpty = lock.newCondition();
//队列非满,创建Condition,在执行take操作成功后使用
notFull = lock.newCondition();
}
在这里补充下Condition知识
Condition,为JDK1.5之后新出显得一个类,await(),signal()用来替换传统的Object的wait(),notify()操作,需要搭配Lock锁使用
创建方式
Lock lock=new ReentrantLock();
Condition condition1=lock.newCondition();
方法
通过查看源码,可知第二个参数fair,主要用于读写锁是否公平,在队列满时进行堵塞后,如果有元素移出队列,如果为公平锁,则按照阻塞的先后顺序入队,FIFO原则;如果为非公平锁,则随机释放阻塞线程,线程之间会有竞争
添加元素
public void put(E e) throws InterruptedException {
//判断入队元素是否为null
checkNotNull(e);
//定义可重入锁
final ReentrantLock lock = this.lock;
//线程中断
lock.lockInterruptibly();
try {
//判断队列是否已满
while (count == items.length)
//队列满进入无限等待状态,直到有元素出队
notFull.await();
//执行入队操作
enqueue(e);
} finally {
//释放锁
lock.unlock();
}
}
checkNotNull方法源码
//传入非空判断
private static void checkNotNull(Object v) {
//判断入队元素是否为null值
if (v == null)
//抛出异常
throw new NullPointerException();
}
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)
//如果是队列最后一个,下次添加将从0开始
putIndex = 0;
//队列执行++操作
count++;
//通知阻塞线程
notEmpty.signal();
}
从添加的源码可知,添加有两种情况
1.添加的元素不在队尾,元素直接入队
2.添加元素索引在队尾,下次添加元素直接从队头索引为0的位置开始
if (++putIndex == items.length)
putIndex = 0;
拿数据
public E take() throws InterruptedException {
//定义可重入锁
final ReentrantLock lock = this.lock;
//线程中断
lock.lockInterruptibly();
try {
//判断队列中元素个数是否为0
while (count == 0)
//进入无限等待操作,直到队列中有元素入队
notEmpty.await();
//调用出队方法
return dequeue();
} finally {
//释放锁
lock.unlock();
}
}
dequeue方法源码
private E dequeue() {
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
final Object[] items = this.items;
@SuppressWarnings("unchecked")
//x 为出队元素的索引位置
E x = (E) items[takeIndex];
//出队索引位置的数据置为null
items[takeIndex] = null;
//判断出队索引是否为最后一个
if (++takeIndex == items.length)
//下次出队从队头索引为0的位置开始
takeIndex = 0;
//队列大小执行-1操作
count--;
if (itrs != null)
itrs.elementDequeued();
notFull.signal();
return x;
}
从源码可知,每次拿出数据,队列的takeIndex值+1,如果takeIndex索引位于队列末尾位置时,下次take操作从队头索引的位置为0处开始。代码
if (++takeIndex == items.length)
takeIndex = 0;
删除元素
void removeAt(final int removeIndex) {
// assert lock.getHoldCount() == 1;
// assert items[removeIndex] != null;
// assert removeIndex >= 0 && removeIndex < items.length;
final Object[] items = this.items;
//情况1:移除元素索引等于拿元素索引
if (removeIndex == takeIndex) {
// removing front item; just advance
//将takeIndex处索引元素置为null,即删除元素
items[takeIndex] = null;
//判断takeIndex索引是否为队尾
if (++takeIndex == items.length)
//在队尾,下次拿元素从0开始
takeIndex = 0;
//队列大小执行-1操作
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;;) {
//删除元素的下一个位置为next
int next = i + 1;
//判断next索引是否位于队尾
if (next == items.length)
//位于队尾下次从0开始
next = 0;
//情况2:下一个元素不等于putIndex
if (next != putIndex) {
//下一个元素往前移动
items[i] = items[next];
i = next;
//情况3:下一个元素等于putIndex
} else {
//删除该索引位置的元素
items[i] = null;
//下次执行put操作的索引位置为删除元素的索引位置
this.putIndex = i;
break;
}
}
//队列大小-1
count--;
if (itrs != null)
itrs.removedAt(removeIndex);
}
//唤醒阻塞线程
notFull.signal();
}
通过源码可知,删除元素新增删除索引:removeIndex,一共有两种情况:
第一种:removeIndex == takeIndex
第二种情况又分为两种情况
1.removeIndex+1!=putIndex,下一个元素往前移动一位
2.如果 removeIndex + 1 == putIndex 的话,就把 putIndex 的值修改成删除的位置