本文已参与「新人创作礼」活动,一起开启掘金创作之路。
什么叫做阻塞队列?
- 一种线性结构,FIFO先进先出,可以两端操作,一边添加另一边删除
- 支持阻塞插入和阻塞取出(例如:上一章演示的生产消费者模式)
- 列表大小分为有界队列和无界队列,无界队列在Java中最大Integer.MAX
本文主要以ArrayBlockingQueue
阻塞队列展开,其他的大同小异,自行了解
1.J.U.C中的阻塞队列
- ArrayBlockingQueue 底层数组结构
final Object[] items;
- LinkedBlockingQueue 底层链表结构
static class Node<E> {
E item;
Node<E> next;
Node(E x) { item = x; }
}
- PriorityBlockingQueue 底层数据结构,基于优先级队列(比较),类似排行榜
private transient Object[] queue;
private transient Comparator<? super E> comparator;
- DelayQueue 延时队列,底层基于优先级队列,类似RocketMq中的延时队列
private final PriorityQueue<E> q = new PriorityQueue<E>();
- SynchronousQueue 里面无任何存储结构,直达的不做存储,另一端不存在时则一直阻塞,在线程池newCacheThread中使用到,底层分为队列或者栈
public SynchronousQueue(boolean fair) {
//公平就用的队列传输数据 否则就用stack栈传输
transferer = fair ? new TransferQueue<E>() : new TransferStack<E>();
}
Queue队列,FIFO先进先出。Stack栈, 后进先出
LinkedTransferQueue 无界阻塞队列等同于 ArraysBlockingQueue + TransferQueue
2.阻塞队列中的常用方法
- 添加元素
add() 如果队列满了,则抛出异常 offer() 添加成功返回true,反之false put() 如果队列满了,则阻塞 offer(timeout) 队列满了就阻塞,但是带有超时时间
- 移除元素
ekement() 如果队列空了,则抛出异常 peek() 移除成功返回true,反之false take() 一直阻塞等待队列有值 poll(timeout) 阻塞等待有值,但带有超时时间
3.阻塞队列的实际使用
- 责任链
- 生产者、消费者
//初始化一个size为3的阻塞队列
static ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
int i = 0;
while (true) {
try {
//队列满了,则阻塞
queue.put("元素" + i);
System.out.println("存入:元素" + i);
} catch (InterruptedException e) {
e.printStackTrace();
}
i++;
}
}).start();
Thread.sleep(100);
new Thread(() -> {
while (true) {
try {
// 一直阻塞等待队列有值
String take = queue.take();
System.out.println("取出:" + take);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
4.阻塞队列源码分析
1.成员变量
//队列结果就是一个数组
final Object[] items;
//下次取出的index 阻塞队列是在加锁代码里面执行的 是安全的
/** items index for next take, poll, peek or remove */
int takeIndex;
//下一次添加的index
/** items index for next put, offer, or add */
int putIndex;
//队列当前的size
/** Number of elements in the queue */
int count;
//重入锁
/** Main lock guarding all access */
final ReentrantLock lock;
//队列不为空 消费者Condition
private final Condition notEmpty;
//队列没满 生产者的Condition
/** Condition for waiting puts */
private final Condition notFull;
2.构造方法
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
//初始化队列大小
this.items = new Object[capacity];
//可以自己定义公平和非公平锁
lock = new ReentrantLock(fair);
//熟悉的condition队列 一个空 一个满
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
3.put() 添加元素
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
//会调用到AbstractQueuedSynchronizer#acquireInterruptibly去
//接下来就是AQS中的抢占锁,没抢占到的加到AQS队列,等待unlock去唤醒逻辑了
lock.lockInterruptibly();
try {
//当前元素个数等于数组长度 队列满了
while (count == items.length)
//阻塞 上面Condition源码有分析 先加到Condition等待队列 然后完全释放锁
//然后再判断状态为CONDITION时阻塞 在唤醒会先同步到AQS队列中 然后唤醒线程
//线程在while中判断已经在同步队列而跳出循环,接下来继续去抢占锁 抢到就从这往下执行
notFull.await();
enqueue(e);
} finally {
lock.unlock();
}
}
- enqueue()添加元素并唤醒消费线程
private void enqueue(E x) {
final Object[] items = this.items;
//赋值到指定的下标 因为在加锁的代码里面 这里是线程安全的
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0;
count++;
//唤醒所有的消费线程
//doSignal中do while把Condition队列中的线程添加到AQS队列
//然后再进行锁的抢占 唤醒抢占到锁的线程 LockSupport.unpark(node.thread);
notEmpty.signal();
}
4.take() 取出元素
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
//先抢占锁
lock.lockInterruptibly();
try {
//如果队列为空 则阻塞
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
- dequeue() 取出元素并唤醒生产线程
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;
}
阻塞队列的总结
总体来说,了解了前面锁的原理后,这个代码看起来应该很简单了。
阻塞队列是一种线性结构,FIFO先进先出,支持两端操作,一边添加一边删除
支持阻塞插入(队列满了就阻塞),支持阻塞取出(队列空了就阻塞)
原理是利用java中的Condition的对个线程队列去实现,当put元素队列满了的情况下,
会先添加到Condition队列,然后完全的释放锁,在while循环中应CONDITION状态阻塞当前线程
唤醒从CONDITION等待队列中do while 去唤醒线程,先把等待队列的线程同步到AQS队列,然后再进行锁的抢占
以上就是本章的全部内容了。
上一篇:线程通信synchronized中的wait/notify、J.U.C Condition的使用和源码分析 下一篇:J.U.C中的工具类及原理分析(CountDownLatch、Semaphore、CyclicBarrier)
读书百遍,其义自见