BlockingQueue浅析

背景

在设计任务处理系统的时候,很自然地想到使用生产/消费者模式。任务由生产者生产完成,然后交由消费者(通常是业务相关的处理器)进行消费,完成任务的处理。由于生产者和消费者的处理能力不可能完全一致,参考实际生活中生产线或工厂库存,可使用Queue来对二者进行隔离。生产者将任务生产完毕之后,不是直接交由消费者来进行立即消费,而是将其加入到Queue中;消费者从Queue中获取任务,然后进行任务分配处理。通过Queue进行隔离之后,生产者和消费者的数目可以不同,通常而言,消费者会是任务处理中的瓶颈,因此这种方式更适宜少生产者,多消费者的业务场景。

常用BlockingQueue分析

常用的BlockingQueue有ArrayBlockingQueue和LinkedBlockingQueue两种。

有关BlockingQueue接口中所定义的方法可参考其源码,为方便归纳,我们按照是否会阻塞、当队列满或空时是否抛异常,将入队/出队方法分成以下三类。

 add/removeoffer/pollput/take
队列满或队列空时是否抛出异常
是否阻塞添加/获取

下边我们对这两种BlockingQueue展开详细的介绍。

ArrayBlockingQueue

其特点是存储介质为数组,即Queue中的元素存储在数组中。

add/remove方法

add/remove方法对于队列满或空时的处理方式一样--抛异常。这种调用方式,通常而言并不优雅,因此使用场景也比较少见。

先来看add方法:其调用父类AbstractQueue的add方法,并在其内部调用offer方法。当队列没有满的时候通过offer方法正常加入元素,如果队列已满抛异常。

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

接下来看remove方法:其实际是调用的父类AbstractQueue中的remove方法。当队列为空时,poll方法返回的x为null,因此抛异常;如果不为空,则正常返回poll方法获取的队列元素x。

 public E remove() {
        E x = poll();
        if (x != null)
            return x;
        else
            throw new NoSuchElementException();
    }

offer/poll方法

当队列已满或为空时,调用offer/poll方法为非阻塞的,通过返回值来告知调用方是否完成入队操作(true or false)或是否获取到队列中的元素(null or 非null)。这种方式相对而言比较优雅,所以在实际场景中比较常见。

首先来看一下offer方法:首先获取锁,注意获取到的是ReentrantLock,而不是synchronized,原因是ReentrantLock是可重入的,并且可设置锁超时,可避免synchronized引发的死锁等问题。接下来检查是否队列已满,如果已满,返回false,表示本次入队操作失败;否则进行入队操作。最后解锁。

    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();
        }
    }

其中的入队方法--enqueue的操作逻辑如下所示。将传入的元素置入游标--putIndex位置,将putIndex自增1。如果游标已达到最后一个元素的位置,则变更到数组的0位置。每次入队操作完成之后,调用notEmpty Condition的通知方法(这是一个注册在锁上的Condition),通知其他线程队列已有数据插入,可以进行消费。

  /**
     * Inserts element at current put position, advances, and signals.
     * Call only when holding lock.
     */
    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++;
        notEmpty.signal();
    }

接下来看一下poll方法,其执行逻辑与offer相反。首先获取锁,然后检验当前队列中的元素个数。如果为0,表示队列为空,返回null;否则执行出队操作。

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

出队操作方法执行逻辑如下。出队操作方法中,使用了takeIndex作为队列数据消费的游标,初始值为0。每次完成出队消费时,将takeIndex位置的元素变为null,然后将游标自增1。当游标达到队列尾部之后,设置为0。最后通知其他线程,队列不满了(notFull.signal)。 

private E dequeue() {
        // assert lock.getHoldCount() == 1;
        // assert items[takeIndex] != null;
        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;
    }

put/take方法

再来看一下阻塞版的入队和出队方法,put和take。这种在实际使用中也比较常见,主要用于生产者-消费者模式的实现。

首先看put方法,如下所示。

public void put(E e) throws InterruptedException {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == items.length)
                notFull.await();
            enqueue(e);
        } finally {
            lock.unlock();
        }
    }

put方法在实现过程中,与offer方法不同的一点在于,若队列已满,则阻塞;直到队列不满时,执行入队操作。

take是与put方法相对应的阻塞地获取队列头部元素的方法。

 public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0)
                notEmpty.await();
            return dequeue();
        } finally {
            lock.unlock();
        }
    }

LinkedBlockingQueue

顾名思义,其特点是存储介质为链表数据结构。

add/remove方法

其add/remove方法是使用的父类AbstractQueue的方法,在此不再赘述。

offer/poll方法

offer与poll方法的执行逻辑,与ArrayBlockingQueue比较类似。不同的地方在于,LinkedBlockingQueue在底层是使用链表进行存储的,链表节点抽象为如下结构:

static class Node<E> {
        E item;

        /**
         * One of:
         * - the real successor Node
         * - this Node, meaning the successor is head.next
         * - null, meaning there is no successor (this is the last node)
         */
        Node<E> next;

        Node(E x) { item = x; }
    }

即保存有元素item及后继节点next,也就是说,LinkedBlockingQueue的存储介质是单向链表(因为仅保留有后继节点的引用)。

offer方法执行逻辑如下:

public boolean offer(E e) {
        if (e == null) throw new NullPointerException();
        final AtomicInteger count = this.count;
        if (count.get() == capacity)
            return false;
        int c = -1;
        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;
    }

首先检测是否队列已满,已满的话直接返回false;否则将元素build成链表节点,获取入队锁,将链表节点入队,递增队列元素个数。如果递增后的元素个数+1后仍小于队列容量,通知其他线程队列非空。完成以上操作之后,解锁,对于加入第一个元素的情况,通知等待take锁的线程队列非空。

接下来关注poll方法,其实现逻辑与offer的实现逻辑相反。

当队列中为空时,返回null;否则尝试获取take锁,执行出队操作(将头结点摘除,将第二结点作为新的头结点)。出队后,只要队列中仍有元素,则通知等待take锁的线程队列非空。执行完毕之后,解锁。

 public E poll() {
        final AtomicInteger count = this.count;
        if (count.get() == 0)
            return null;
        E x = null;
        int c = -1;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
            if (count.get() > 0) {
                x = dequeue();
                c = count.getAndDecrement();
                if (c > 1)
                    notEmpty.signal();
            }
        } finally {
            takeLock.unlock();
        }
        if (c == capacity)
            signalNotFull();
        return x;
    }

put/take方法

最后我们来看一下阻塞版的入队和出队方法。

public void put(E e) throws InterruptedException {
        if (e == null) throw new NullPointerException();
        // Note: convention in all put/take/etc is to preset local var
        // holding count negative to indicate failure unless set.
        int c = -1;
        Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
        putLock.lockInterruptibly();
        try {
            /*
             * Note that count is used in wait guard even though it is
             * not protected by lock. This works because count can
             * only decrease at this point (all other puts are shut
             * out by lock), and we (or some other waiting put) are
             * signalled if it ever changes from capacity. Similarly
             * for all other uses of count in other wait guards.
             */
            while (count.get() == capacity) {
                notFull.await();
            }
            enqueue(node);
            c = count.getAndIncrement();
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            putLock.unlock();
        }
        if (c == 0)
            signalNotEmpty();
    }

其中体现阻塞的语句是:

notFull.await。

相类似的,take方法的实现如下所示。其中体现阻塞的语句是:

notEmpty.await。

 public E take() throws InterruptedException {
        E x;
        int c = -1;
        final AtomicInteger count = this.count;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lockInterruptibly();
        try {
            while (count.get() == 0) {
                notEmpty.await();
            }
            x = dequeue();
            c = count.getAndDecrement();
            if (c > 1)
                notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
        if (c == capacity)
            signalNotFull();
        return x;
    }

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值