J.U.C中的阻塞队列使用及源码分析--ArrayBlockingQueue

392 阅读4分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

什么叫做阻塞队列?

  • 一种线性结构,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)

读书百遍,其义自见