深入浅出 JUC 之阻塞队列

133 阅读2分钟

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

小伙伴们大家好,这里是追風者。今天我们来聊一聊阻塞队列。

阻塞队列,是一种线程安全的、用于线程排队的一种队列。JUC 提供了其中阻塞队列,下面逐个分析。

ArrayBlockingQueue

数组阻塞队列,底层使用数组结构实现。ArrayBlockinhQueue 是有界阻塞队列,构造方法中对容量进行初始化。

    public ArrayBlockingQueue(int capacity) {
        this(capacity, false);
    }
    
    // 初始化数组,默认采用非公锁,初始化出两个条件队列
    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();
    }
    

入队操作

ArrayBlockingQueue 继承了 AbstractQueue 抽象类,AbstractQueue 采用的是模板方法模式,子类实现 Queue 接口,实现 Queue 中的方法来完成功能。

add() 方法

add() 方法在入队失败会抛出异常。

    public boolean add(E e) {
        if (offer(e))
            return true;
        else
            throw new IllegalStateException("Queue full");
    }

    // 插入 e
    public boolean offer(E e) {
        // 校验空
        checkNotNull(e);
        // 可重入锁
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            // 队列中元素与 数组长度相等时,入队失败
            if (count == items.length)
                return false;
            else {
                // 入队
                enqueue(e);
                return true;
            }
        } finally {
            lock.unlock();
        }
    }
    
    // 将 x 加入到数组中。
    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)
            putIndex = 0;
        count++;
        // 可重入锁的 Conditon signal
        // 唤醒 notEmpty 中的首节点进入到阻塞队列中,下次线程争抢锁时,就能执行了。
        notEmpty.signal();
    }

put() 方法

与 add() 方法比较,put() 方法是响应中断的,能够抛出中断异常,采用的锁也是可中断锁。并且,put() 方法在队列为满的时候,入队失败会阻塞线程。

    // 将 e 加入到 数组队列中。
    public void put(E e) throws InterruptedException {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        // 响应中断的上锁
        lock.lockInterruptibly();
        try {
            // 当 count 与数组长度相等,就进入条件队列
            while (count == items.length)
                // 将线程加入到条件队列中。阻塞线程
                notFull.await();
            // e 加入到数组中。
            enqueue(e);
        } finally {
            lock.unlock();
        }
    }
    
    // 将 x 加入到数组中。
    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)
            putIndex = 0;
        count++;
        // 可重入锁的 Conditon signal
        // 唤醒 notEmpty 中的首节点进入到阻塞队列中,下次线程争抢锁时,就能执行了。
        notEmpty.signal();
    }

offer() 方法

由于 add() 使用的就是 offer() 方法,就不在这里赘述了。

出队操作

take() 方法

take() 方法是响应中断的,采用的是可中断锁。当队列为空时,线程会阻塞。

    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() {
        // assert lock.getHoldCount() == 1;
        // assert items[takeIndex] != null;
        final Object[] items = this.items;
        @SuppressWarnings("unchecked")
        // take 为出队索引
        E x = (E) items[takeIndex];
        items[takeIndex] = null;
        if (++takeIndex == items.length)
            takeIndex = 0;
        count--;
        // itrs 迭代器不为空
        if (itrs != null)
            //
            itrs.elementDequeued();
        notFull.signal();
        return x;
    }

poll() 方法

与 tabke() 方法对比,poll() 在队列为空时,不会阻塞线程。

// 出队
    public E poll() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            // dequeue() 出队
            return (count == 0) ? null : dequeue();
        } finally {
            lock.unlock();
        }
    }

总结

我们可以看到,ArrayBlockingQueue 在插入到队列满状态时,要么是阻塞线程,要么直接抛出异常,所以它是有界队列。

操作方法特点
入队add()队列满会抛出异常
入队offer()入队会唤醒 notEmpty 条件队列,唤醒 notEmpty 队列
入队put()队列满会阻塞线程,将线程加入到 notFull 条件队列中
出队take()队列为空时,会将线程加入到 notEmpty 条件队列中;唤醒 notFull 队列
出队poll()队列为空时,直接返回空;唤醒 notFull 队列

通过上面的源码分析我们可以看出,add()、put() 底层都是调用的 offer(),区别在于调用后对结果的判定情况。take() 与 poll() 也是如此。

ArrayBlockingQueue 是通过一个 ReentrantLock 创建的两个条件队列,所以可以通过获取一个锁,对两个条件队列进行阻塞/唤醒来维护队列模型。

LinkedBlockingQueue

LinkedBlockingQueue 底层是单向链表。同样是有界队列。与 ArrayBlockingQueue 一样,继承了 AbstractQueue,采用其模板方法的方式实现功能。并且 linkedBlockingQueue 是链表结构,入队出队可以分离开,所以它实现了两个锁和两个队列,一个为入队锁、入队队列,一个为出对锁、出队队列。

    // 默认的容量为最大值
    public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }
    
    // 构造方法对队列的首尾节点进行初始化。
    public LinkedBlockingQueue(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;
        last = head = new Node<E>(null);
    }

插入操作

由于与 ArrayBlockingQueue 类似,就直接放出 offer() 方法的代码。add() 方法通过模板方法调用 offer()。

    // e 入队
    public boolean offer(E e) {
        if (e == null) throw new NullPointerException();
        final AtomicInteger count = this.count;
        // 当 元素数量与 阈值相等时,就返回 false 插入失败
        if (count.get() == capacity)
            return false;
        int c = -1;
        // 构建 e 的 Node
        Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
        putLock.lock();
        try {
            if (count.get() < capacity) {
                // 入队
                enqueue(node);
                c = count.getAndIncrement();
                if (c + 1 < capacity)
                    notFull.signal();
            }
        } finally {
            putLock.unlock();
        }
        if (c == 0)
            signalNotEmpty();
        return c >= 0;
    }
    
    // 入队操作
    private void enqueue(Node<E> node) {
        // assert putLock.isHeldByCurrentThread();
        // assert last.next == null;
        last = last.next = node;
    }

put() 方法入队:

    // 响应中断
    public void put(E e) throws InterruptedException {
        if (e == null) throw new NullPointerException();
        int c = -1;
        Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
        putLock.lockInterruptibly();
        try {
            while (count.get() == capacity) {
                notFull.await();
            }
            enqueue(node);
            c = count.getAndIncrement();
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            putLock.unlock();
        }
        if (c == 0)
            // 唤醒 noteEmpty 队列
            signalNotEmpty();
    }

    // 由于两个锁分别创建的队列,此时就需要获取到出队锁。
    private void signalNotEmpty() {
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
            notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
    }

出队操作

出队时,take() 与 poll() 的区别在于 take() 在队列空时能够阻塞线程。

总结

LinkedBlockingQueue 与 ArrayBlockingQueue 相比,方法的大体实现思路都是一致的,只不过由于链表的性质, LinkedBlockingQueue 维护了两个锁和两个条件队列。

LinkedBlockingDeque

LinkedBlockingDeque 底层是双向链表,它可以头查尾插、头删尾删。仍然采用一个锁两个同步队列的模式。锁是默认的非公平锁,通过 ConditionObject 来实现响应中断。

PriorityBlockingQueue

PriorityBlockingQueue 是一个无界队列,底层使用数组实现,当数组满的时候,它可以尝试进行扩容。默认的数组容量为 11。 PriorityBlockingQueue 的特点是,入队的数据必须要实现 compareTo 接口,否则运行时会抛出异常。队列会根据默认的比较器或者构造时传入的比较器进行排序,此排序在迭代器遍历时,不一定有序,但 poll()、take() 等方法会有序出队。

SynchronousQueue

SynchronousQueue 有两种模式,分别为 Stack 和 Queue。SynchronousQueue 插入元素后就会阻塞,直到有出队操作。 足以看出,SynchronousQueue 只能有一个元素在队列中。

DelayQueue

DelayQueue 是一个无界队列,内部维护了 PriorityQueue,PriorityQueue 就是非线程安全的 PriorityBlockingQueue。DelayQueue 主要是作为延时队列进行使用,入队的节点必须实现 Delayed 接口。DelayQueue 入队操作主要还是调用的 PriorityQueue 的 offer() 方法。DelayQueue 出队时,会对元素的延时进行校验 getDelay(NANOSECONDS) > 0,如果未到时间就阻塞条件队列或返回空值(看具体方法实现,poll() 会直接返回 null,而take() 会阻塞条件队列)。

LinkedTransferQueue

LinkedTransferQueue 为无界的阻塞队列。它的特点是 transfer() 方法和 tryTransfer() 方法。transfer() 方法可以直接把生产者的元素立即传输给消费者,如果没有消费者接收,transfer() 方法会将元素存放在队列的尾节点,等到该元素被消费才返回。tryTransfer() 是用来试探生产者传入的元素能够直接传给消费者。

阻塞队列的入队出队操作,节点都会延迟删除。