BlockingQueue 阻塞队列详解

·  阅读 982
BlockingQueue 阻塞队列详解

本文已参与掘金创作者训练营第三期「话题写作」赛道,详情查看:掘力计划|创作者训练营第三期正在进行,「写」出个人影响力

BlockingQueue 是什么?

BlockingQueue 是一个 Queue , 它是一个线程安全的阻塞队列接口。 ​

一种队列,它还支持在检索元素时等待队列变为非空,在存储元素时等待队列中的空间变为可用的操作。 BlockingQueue方法有四种形式,有不同的处理操作的方法,这些操作不能立即满足,但在将来的某个时候可能会满足:一种抛出异常,另一种返回特殊值(null或false,取决于操作),第三个线程无限期地阻塞当前线程,直到操作成功,第四个线程在放弃之前只阻塞给定的最大时间限制。下表总结了这些方法:

抛出异常特殊值阻塞超时
插入add(e)offer(e)put(e)offer(e, time, unit)
移除remove()poll()take()poll(time, unit)
检查element()peek()不可用不可用

下图是它的一个继承和实现关系图: BlockingQueue.png

常见的几种队列

  1. ArrayBlockingQueue 数组有界队列
  2. LinkedBlockingDeque 链表无界队列
  3. DelayQeque 基于时间的调度无界队列
  4. PriorityBlockingQueue 优先级堆支持的无界队列

使用场景

  1. 线程池中使用,下面是咱们线程池的构造方法如下:
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    //...
}
复制代码
  1. Eureka 三级缓存
  2. Netty
  3. Nacos
  4. RokcetMQ

ArrayBlockingQueue

由数组支持的有界阻塞队列。该队列对元素进行FIFO排序(先进先出)。队列的头是在队列上停留时间最长的元素。队列的尾部是在队列上停留时间最短的元素。新元素插入到队列的尾部,队列检索操作获取队列头部的元素。 ​

这是一个经典的“有界缓冲区”,其中一个固定大小的数组保存生产者插入的元素和消费者提取的元素。一旦创建,容量就无法更改。尝试将元素放入完整队列将导致操作阻塞;尝试从空队列中获取元素也会被阻塞。 ​

此类支持一个可选的公平策略,用于排序等待的生产者线程和消费者线程。默认情况下,不保证此顺序。然而,公平性设置为true的队列以FIFO顺序授予线程访问权限。公平性通常会降低吞吐量,但会降低可变性并避免饥饿。

数据结构

它底层的数据结构是一个数组形式,构造方法如下:

public ArrayBlockingQueue(int capacity, boolean fair) {
    if (capacity <= 0)
        throw new IllegalArgumentException();
    this.items = new Object[capacity]; // 初始化数组
    lock = new ReentrantLock(fair);    // 创建锁
    notEmpty = lock.newCondition();
    notFull =  lock.newCondition();
}
复制代码

入队和出队过程

入队和出队过程如下图所示(流程图为 put/take 方法),它的本质是一个设置一个全局的Lock , 它是一个 ReentrantLock 然后通过 Condition 进行边界状态的限制,就是进行条件通知。 image.png

使用场景

通常在线程池创建的时候,我一般会使用 LinkedBlockingDeque 作为一个缓冲队列。

LinkedBlockingDeque

LinkedBlockingDeque是一个由链表结构组成的双向阻塞队列,即可以从队列的两端插入和移除元素。双向队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。 相比于其他阻塞队列,LinkedBlockingDeque多了addFirst、addLast、peekFirst、peekLast等方法,以first结尾的方法,表示插入、获取获移除双端队列的第一个元素。以last结尾的方法,表示插入、获取获移除双端队列的最后一个元素。 LinkedBlockingDeque是可选容量的,在初始化时可以设置容量防止其过度膨胀,如果不设置,默认容量大小为Integer.MAX_VALUE。

数据结构

数据结构如下,它是一个双端单向链表。 image.png

如何使用

下面我们简单的使用一下,测试代码如下所示:

public class LinkedBockingQueueTest {

    public static void main(String[] args) {
        BlockingDeque<String> blockingDeque = new LinkedBlockingDeque<>(1);
        // offer,poll 线程安全/阻塞 api
        blockingDeque.offer("添加第一个元素");
        String item = blockingDeque.poll();
        System.out.println("poll item:" + item);

        // offer,poll 线程安全/如果失败抛出异常
        try {
            blockingDeque.put("添加第二个元素");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        try {
            String take = blockingDeque.take();
            System.out.println("take item:" + take);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // add,remove 不是线程安全的
        blockingDeque.add("添加第四个元素");
        blockingDeque.add("添加第五个元素");
        item = blockingDeque.remove();
        System.out.println(item);
    }
}
复制代码

输出结果如下所示:

poll item:添加第一个元素
take item:添加第二个元素
Exception in thread "main" java.lang.IllegalStateException: Deque full
	at java.util.concurrent.LinkedBlockingDeque.addLast(LinkedBlockingDeque.java:335)
	at java.util.concurrent.LinkedBlockingDeque.add(LinkedBlockingDeque.java:633)
	at cn.zhengsh.queue.LinkedBockingQueueTest.main(LinkedBockingQueueTest.java:30)

复制代码

使用场景

通常在线程池创建的时候,我一般会使用 LinkedBlockingDeque 作为一个缓冲队列。

DelayQueue

DelayQeque 是一个无界阻塞队列,只有在延迟时间到达的时候,才能从队列中获取元素。可以设置队列元素的存活时间,移除时间,唯一 id 等元素。

源码分析

  • 添加方法 offer
public boolean offer(E e) {
    // 获取锁
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        // 元素加入优先级队列
        q.offer(e);
        // 获取优先级头元素,头元素等于当前元素
        // 清空leader,并放开读限制
        if (q.peek() == e) {
            leader = null;
            available.signal();
        }
        return true;
    } finally {
        // 释放锁
        lock.unlock();
    }
}
复制代码
  • 出队方法 take, 如果为空当前线程阻塞
public E take() throws InterruptedException {
    // 获取锁
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        // 自旋
        for (;;) {
            // 获取优先级队列头节点
            E first = q.peek();
            // 优先级队列为空
            if (first == null)
                // 阻塞
                available.await();
            else {
                // 判断头元素剩余时间是否小于等于0
                long delay = first.getDelay(NANOSECONDS);
                if (delay <= 0)
                    // 优先级队列出队
                    return q.poll();
                // 到这,说明剩余时间大于0
                // 头引用置空
                first = null;
                // leader线程是否为空,不为空就等待
                if (leader != null)
                    available.await();
                else {
                    // 设置leader线程为当前线程
                    Thread thisThread = Thread.currentThread();
                    leader = thisThread;
                    try {
                        // 休眠剩余秒
                        available.awaitNanos(delay);
                    } finally {
                        // 休眠结束,leader线程还是当前线程
                        // 置空leader
                        if (leader == thisThread)
                            leader = null;
                    }
                }
            }
        }
    } finally {
        // leader线程为空,并且first不为空
        // 唤醒阻塞的leader,让它再去试一次
        if (leader == null && q.peek() != null)
            available.signal();
        // 解锁
        lock.unlock();
    }
}
复制代码

使用场景

我一般很少直接使用它,但是在我们使用的框架中大量使用。

PriorityBlockingQueue

在这个数据结构,元素是按照顺序储存的。元素们必须实现带有 compareTo() 方法的 Comparable 接口。当你在结构中插入数据时,它会与数据元素对比直到找到它的位置。

源码分析

构造方法 PriorityBlockingQueue(Collection<? extends E> c) 分析,如下所示

/**
 * 从已有集合构造队列.
 * 如果已经集合是SortedSet或者PriorityBlockingQueue, 则保持原来的元素顺序
 */
public PriorityBlockingQueue(Collection<? extends E> c) {
    this.lock = new ReentrantLock();
    this.notEmpty = lock.newCondition();
    boolean heapify = true;     // true if not known to be in heap order
    boolean screen = true;      // true if must screen for nulls
 
    if (c instanceof SortedSet<?>) {                        // 如果是有序集合
        SortedSet<? extends E> ss = (SortedSet<? extends E>) c;
        this.comparator = (Comparator<? super E>) ss.comparator();
        heapify = false;
    } else if (c instanceof PriorityBlockingQueue<?>) {     // 如果是优先级队列
        PriorityBlockingQueue<? extends E> pq = (PriorityBlockingQueue<? extends E>) c;
        this.comparator = (Comparator<? super E>) pq.comparator();
        screen = false;
        if (pq.getClass() == PriorityBlockingQueue.class)   // exact match
            heapify = false;
    }
 
    Object[] a = c.toArray();
    int n = a.length;
    if (a.getClass() != Object[].class)
        a = Arrays.copyOf(a, n, Object[].class);
    if (screen && (n == 1 || this.comparator != null)) {    // 校验是否存在null元素
        for (int i = 0; i < n; ++i)
            if (a[i] == null)
                throw new NullPointerException();
    }
    this.queue = a;
    this.size = n;
    if (heapify)    // 堆排序
        heapify();
}
复制代码
  • 插入元素 offer 方法分析
public boolean offer(E e) {
    if (e == null)
        throw new NullPointerException();
 
    final ReentrantLock lock = this.lock;   // 加锁
    lock.lock();
 
    int n, cap;
    Object[] array;
    while ((n = size) >= (cap = (array = queue).length))    // 队列已满, 则进行扩容
        tryGrow(array, cap);
 
    try {
        Comparator<? super E> cmp = comparator;
        if (cmp == null)    // 比较器为空, 则按照元素的自然顺序进行堆调整
            siftUpComparable(n, e, array);
        else                // 比较器非空, 则按照比较器进行堆调整
            siftUpUsingComparator(n, e, array, cmp);
        size = n + 1;       // 队列元素总数+1
        notEmpty.signal();  // 唤醒一个可能正在等待的"出队线程"
    } finally {
        lock.unlock();
    }
    return true;
}
复制代码

上面最关键的是siftUpComparable和siftUpUsingComparator方法,这两个方法内部几乎一样,只不过前者是一个根据元素的自然顺序比较,后者则根据外部比较器比较,我们重点看下siftUpComparable方法:

/**
 * 将元素x插入到array[k]的位置.
 * 然后按照元素的自然顺序进行堆调整——"上浮",以维持"堆"有序.
 * 最终的结果是一个"小顶堆".
 */
private static <T> void siftUpComparable(int k, T x, Object[] array) {
    Comparable<? super T> key = (Comparable<? super T>) x;
    while (k > 0) {
        int parent = (k - 1) >>> 1;     // 相当于(k-1)除2, 就是求k结点的父结点索引parent
        Object e = array[parent];
        if (key.compareTo((T) e) >= 0)  // 如果插入的结点值大于父结点, 则退出
            break;
 
        // 否则,交换父结点和当前结点的值
        array[k] = e;
        k = parent;
    }
    array[k] = key;
}
复制代码

siftUpComparable方法的作用其实就是堆的“上浮调整”,可以把堆可以想象成一棵完全二叉树,每次插入元素都链接到二叉树的最右下方,然后将插入的元素与其父结点比较,如果父结点大,则交换元素,直到没有父结点比插入的结点大为止。这样就保证了堆顶(二叉树的根结点)一定是最小的元素。(注:以上仅针对“小顶堆”)

  • 拓容 tryGrow方法
private void tryGrow(Object[] array, int oldCap) {
    lock.unlock();  // 扩容和入队/出队可以同时进行, 所以先释放全局锁
    Object[] newArray = null;
    if (allocationSpinLock == 0 &&
            UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,
                    0, 1)) {    // allocationSpinLock置1表示正在扩容
        try {
            // 计算新的数组大小
            int newCap = oldCap + ((oldCap < 64) ?
                    (oldCap + 2) :
                    (oldCap >> 1));
            if (newCap - MAX_ARRAY_SIZE > 0) {    // 溢出判断
                int minCap = oldCap + 1;
                if (minCap < 0 || minCap > MAX_ARRAY_SIZE)
                    throw new OutOfMemoryError();
                newCap = MAX_ARRAY_SIZE;
            }
            if (newCap > oldCap && queue == array)
                newArray = new Object[newCap];  // 分配新数组
        } finally {
            allocationSpinLock = 0;
        }
    }
    if (newArray == null)   // 扩容失败(可能有其它线程正在扩容,导致allocationSpinLock竞争失败)
        Thread.yield();
    
    lock.lock();            // 获取全局锁(因为要修改内部数组queue)
    if (newArray != null && queue == array) {
        queue = newArray;   // 指向新的内部数组
        System.arraycopy(array, 0, newArray, 0, oldCap);
    }
}
复制代码
  • 出对 take() 方法分析
/**
 * 出队一个元素.
 * 如果队列为空, 则阻塞线程.
 */
public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();   // 获取全局锁
    E result;
    try {
        while ((result = dequeue()) == null)    // 队列为空
            notEmpty.await();                   // 线程在noEmpty条件队列等待
    } finally {
        lock.unlock();
    }
    return result;
}
 
private E dequeue() {
    int n = size - 1;   // n表示出队后的剩余元素个数
    if (n < 0)          // 队列为空, 则返回null
        return null;
    else {
        Object[] array = queue;
        E result = (E) array[0];    // array[0]是堆顶结点, 每次出队都删除堆顶结点
        E x = (E) array[n];         // array[n]是堆的最后一个结点, 也就是二叉树的最右下结点
        array[n] = null;
        Comparator<? super E> cmp = comparator;
        if (cmp == null)
            siftDownComparable(0, x, array, n);
        else
            siftDownUsingComparator(0, x, array, n, cmp);
        size = n;
        return result;
    }
}
复制代码

使用场景

我一般很少直接使用它,但是在我们使用的框架中大量使用。

参考资料

分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改