Java并发包concurrent——BlockingQueue

目录

1. BlockingQueue接口

2. BlockingQueue的分类

3. 有界阻塞队列——ArrayBlockingQueue和LinkedBlockingQueue

4. 优先无界阻塞队列——PriorityBlockingQueue

5. 同步阻塞队列——SynchronousQueue

6. 延时阻塞队列——DelayQueue


BlockingQueue是java.util.concurrent包中定义的一个阻塞队列的接口,concurrent包中实现了多种阻塞队列,它们都实现了该接口。有了这些阻塞队列,可以更好的实现多线程之间的数据共享;而如果能灵活运用这些类,便可以在工作中更好的实现高质量的多线程高并发程序。本章首先从BlockingQueue接口开始,详细分析了concurrent包中的这些阻塞队列的实现。

1. BlockingQueue接口

        BlockingQueue接口继承了Queue接口,所以其本身也可以作为一般的队列使用。在此基础上,BlockingQueue定义了如下几个阻塞方法:

  1. void put(E e) throws InterruptedException; 该方法为阻塞式入队方法,如果发生中断异常会抛出。
  2. boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException; 该方法为阻塞式入队方法,该方法可以设置一个超时时间,当发生超时时可返回false表示入队失败。
  3. E take() throws InterruptedException; 该方法为阻塞式出队方法,如果发生中断异常会抛出。
  4. E poll(long timeout, TimeUnit unit) throws InterruptedException; 该方法为阻塞式出对方法,该方法也可以设置一个超时时间,如果发生超时,将会返回null。

2. BlockingQueue的分类

        concurrent包中定义了多种阻塞队列:

  1.  ArrayBlockingQueue:有界阻塞队列,通过数组实现,在创建对象时必须指定该队列的容量。
  2.  LinkedBlockingQueue:有界阻塞队列,通过链表实现,在创建对象时可以指定该队列的容量,如果不指定容量默认为Integer类型的最大值。
  3.  PriorityBlockingQueue:无界阻塞队列,可指定初始容量,可指定队列的优先级。
  4. SynchronousQueue:同步阻塞队列,该阻塞队列比较特殊,其不存储元素,如果该队列中有一个元素,则其他线程不能再往里面插入元素;只有等消费者线程消费之后才能继续插入元素,反之也是一样。
  5. DelayQueue:延时阻塞队列,该队列可设置延时,入队的元素只有在延时期满之后才能消费。
  6. LinkedTransferQueue:无界阻塞队列,通过链表实现,相比其他队列,该队列多了两个方法transfer()和tryTransfer()。
  7. LinkedBlockingDeque:双向阻塞队列,该队列也是通过链表实现,多线程并发时可将锁竞争缩减一半。

        本节将concurrent包中所有的阻塞队列的特点进行了简要的概括,下文将详细分析每种阻塞队列的实现原理与使用场景等。

3. 有界阻塞队列——ArrayBlockingQueue和LinkedBlockingQueue

        concurrent包实现了两种有界阻塞队列:ArrayBlockingQueue和LinkedBlockingQueue,顾名思义,ArrayBlockingQueue通过数组实现阻塞队列,LinkedBlockingQueue则通过链表实现阻塞队列。两者的实现方案类似,因此,在此以ArrayBlockingQueue为例,介绍有界阻塞队列的实现原理。

        在ArrayBlockingQueue中定义了两个方法用来为阻塞队列插入和删除元素。

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

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

        其中,enqueue方法为队列添加元素,dequeue方法为队列删除元素。从这两个方法可以看出,ArrayBlockingQueue中定义了一个items数组用来保存阻塞队列的元素,然后分别定义了putIndex和takeIndex来表示插入和删除元素的位置。当putIndex或takeIndex增长为数组的长度时,会将其置零,表示该队列为一个循环队列。ArrayBlockingQueue中的元素个数则用count来统计。ArrayBlockingQueue可通过构造方法指定阻塞队列的容量capacity。

        ArrayBlockingQueue基于ReentrantLock和Condition来实现阻塞队列和线程安全。ArrayBlockingQueue定义了一个ReentrantLock对象lock和两个Condition对象notEmpty和notFull。首先,在调用阻塞队列相关的方法时,通过ReentrantLock为操作加锁;然后进行入队和出队操作。入队时,如果队列已满,对于阻塞类的入队方法会调用notFull的await等待,直到有线程调用出队方法,此时会触发notFull的signal方法通知阻塞的线程入队。反之,如果队列为空,对于阻塞类的出队方法会调用notEmpty的await方法等待,直到有线程调用入队方法,就会触发notEmpty的signal方法通知阻塞的线程出队。而对于非阻塞类的入队和出对方法,入队和出队则直接返回,不会阻塞。

        LinkedBlockingQueue是基于链表的有界阻塞队列,因为都是有界阻塞队列,所以其实现与ArrayBlockingQueue类似,在此不再赘述其实现原理。这里仅总结一下ArrayBlockingQueue和LinkedBlockingQueue的异同点,以便在平时应用中选择更加合适的队列。

        ArrayBlockingQueue和LinkedBlockingQueue都是有界阻塞队列,也就是说队列的容量有限制,都是通过构造函数来指定队列的容量。而其不同点主要体现在以下几个方面:

  • ArrayBlockingQueue是基于数组实现的,因此队列的每个元素即表示为数组的一个元素;而LinkedBlockingQueue是基于链表实现的,队列的每个元素需要额外的一个Node对象来保存。
  • ArrayBlockingQueue出队和入队使用同一个ReentrantLock对象加锁;而LinkedBlockingQueue的出队和入队使用不同的两个ReentrantLock对象加锁。
  • ArrayBlockingQueue可以在构造函数中指定锁为公平锁,默认为非公平锁。
  • 对于LinkedBlockingQueue,还有一点需要注意,如果创建LinkedBlockingQueue对象时不为其指定容量,则其容量默认为Integer.MAX_VALUE,所以如果使用不当,很可能将系统内存耗尽,使用时需要特别注意,推荐为其指定固定的容量。

4. 优先无界阻塞队列——PriorityBlockingQueue

        PriorityBlockingQueue是一个可以指定优先级的无界阻塞队列,其实现原理是在该队列中添加了一个Comparator比较器用于排序。

        首先,PriorityBlockingQueue是一个无界阻塞队列,因此其在创建时指定的容量只是一个初始容量,随着元素的不断增多,其会调用tryGrow方法进行扩容。

    private void tryGrow(Object[] array, int oldCap) {
        lock.unlock(); // must release and then re-acquire main lock
        Object[] newArray = null;
        if (allocationSpinLock == 0 &&
            UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,
                                     0, 1)) {
            try {
                int newCap = oldCap + ((oldCap < 64) ?
                                       (oldCap + 2) : // grow faster if small
                                       (oldCap >> 1));
                if (newCap - MAX_ARRAY_SIZE > 0) {    // possible overflow
                    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) // back off if another thread is allocating
            Thread.yield();
        lock.lock();
        if (newArray != null && queue == array) {
            queue = newArray;
            System.arraycopy(array, 0, newArray, 0, oldCap);
        }
    }

        扩容时,首先会释放锁,然后计算新数组的容量,然后再为主线程加锁的方式进行。其中,容量过大可能会溢出,抛出OOM异常。

        介绍了扩容之后,接着对队列的入队和出队方法进行介绍。PriorityBlockingQueue使用堆排序算法对元素进行排序。

    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 cmp = comparator;
            if (cmp == null)
                siftUpComparable(n, e, array);
            else
                siftUpUsingComparator(n, e, array, cmp);
            size = n + 1;
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
        return true;
    }

      入队时,如果元素的数量超过了当前的容量,则首先进行扩容操作;然后,会根据比较器进行插入操作。其会调用队列中的siftUpComparable或siftUpUsingComparator方法插入数据,具体根据创建PriorityBlockingQueue对象时是否传入比较器决定。

    private static  void siftUpComparable(int k, T x, Object[] array) {
        Comparable key = (Comparable) x;
        while (k > 0) {
            int parent = (k - 1) >>> 1;
            Object e = array[parent];
            if (key.compareTo((T) e) >= 0)
                break;
            array[k] = e;
            k = parent;
        }
        array[k] = key;
    }

    private static  void siftUpUsingComparator(int k, T x, Object[] array,
                                       Comparator cmp) {
        while (k > 0) {
            int parent = (k - 1) >>> 1;
            Object e = array[parent];
            if (cmp.compare(x, (T) e) >= 0)
                break;
            array[k] = e;
            k = parent;
        }
        array[k] = x;
    }

        由代码可知,PriorityBlockingQueue采用堆排序保持插入元素的顺序,并以此来保证优先级。因为其是一个无界队列,因此元素入队时是不会产生阻塞的。

        元素出队则根据排序的元素顺序进行出队,主要调用了dequeue方法出队。

    private E dequeue() {
        int n = size - 1;
        if (n < 0)
            return null;
        else {
            Object[] array = queue;
            E result = (E) array[0];
            E x = (E) array[n];
            array[n] = null;
            Comparator cmp = comparator;
            if (cmp == null)
                siftDownComparable(0, x, array, n);
            else
                siftDownUsingComparator(0, x, array, n, cmp);
            size = n;
            return result;
        }
    }

        该方法会取数组的第一个元素出队,然后调用对应的siftDownComparable或siftDownUsingComparator方法保持堆的数据结构。

    private static  void siftDownComparable(int k, T x, Object[] array,
                                               int n) {
        if (n > 0) {
            Comparable key = (Comparable)x;
            int half = n >>> 1;           // loop while a non-leaf
            while (k < half) {
                int child = (k << 1) + 1; // assume left child is least
                Object c = array[child];
                int right = child + 1;
                if (right < n &&
                    ((Comparable) c).compareTo((T) array[right]) > 0)
                    c = array[child = right];
                if (key.compareTo((T) c) <= 0)
                    break;
                array[k] = c;
                k = child;
            }
            array[k] = key;
        }
    }

    private static  void siftDownUsingComparator(int k, T x, Object[] array,
                                                    int n,
                                                    Comparator cmp) {
        if (n > 0) {
            int half = n >>> 1;
            while (k < half) {
                int child = (k << 1) + 1;
                Object c = array[child];
                int right = child + 1;
                if (right < n && cmp.compare((T) c, (T) array[right]) > 0)
                    c = array[child = right];
                if (cmp.compare(x, (T) c) <= 0)
                    break;
                array[k] = c;
                k = child;
            }
            array[k] = x;
        }
    }

5. 同步阻塞队列——SynchronousQueue

        SynchronousQueue是一个同步阻塞队列,其本身并不保存元素。每个入队操作都必须等待一个出队操作才能继续下一个入队操作。

        SynchronousQueue队列中实现了两个Transferer接口:TransferStack和TransferQueue,分别用来实现非公平策略和公平策略,这两种类型的底层则都是通过链表来实现的。Transferer接口是一个为双堆栈和队列提供的共享内部API,及在SynchronousQueue中,入队和出对方法都会调用Transferer接口的transfer方法。

    /**
     * Shared internal API for dual stacks and queues.
     */
    abstract static class Transferer {
        /**
         * Performs a put or take.
         *
         * @param e if non-null, the item to be handed to a consumer;
         *          if null, requests that transfer return an item
         *          offered by producer.
         * @param timed if this operation should timeout
         * @param nanos the timeout, in nanoseconds
         * @return if non-null, the item provided or received; if null,
         *         the operation failed due to timeout or interrupt --
         *         the caller can distinguish which of these occurred
         *         by checking Thread.interrupted.
         */
        abstract E transfer(E e, boolean timed, long nanos);
    }

        下面以TransferStack为例介绍SynchronousQueue队列的实现原理。

        /**
         * Puts or takes an item.
         */
        @SuppressWarnings("unchecked")
        E transfer(E e, boolean timed, long nanos) {
            SNode s = null; // constructed/reused as needed
            int mode = (e == null) ? REQUEST : DATA;

            for (;;) {
                SNode h = head;
                if (h == null || h.mode == mode) {  // empty or same-mode
                    if (timed && nanos <= 0) {      // can't wait
                        if (h != null && h.isCancelled())
                            casHead(h, h.next);     // pop cancelled node
                        else
                            return null;
                    } else if (casHead(h, s = snode(s, e, h, mode))) {
                        SNode m = awaitFulfill(s, timed, nanos);
                        if (m == s) {               // wait was cancelled
                            clean(s);
                            return null;
                        }
                        if ((h = head) != null && h.next == s)
                            casHead(h, s.next);     // help s's fulfiller
                        return (E) ((mode == REQUEST) ? m.item : s.item);
                    }
                } else if (!isFulfilling(h.mode)) { // try to fulfill
                    if (h.isCancelled())            // already cancelled
                        casHead(h, h.next);         // pop and retry
                    else if (casHead(h, s=snode(s, e, h, FULFILLING|mode))) {
                        for (;;) { // loop until matched or waiters disappear
                            SNode m = s.next;       // m is s's match
                            if (m == null) {        // all waiters are gone
                                casHead(s, null);   // pop fulfill node
                                s = null;           // use new node next time
                                break;              // restart main loop
                            }
                            SNode mn = m.next;
                            if (m.tryMatch(s)) {
                                casHead(s, mn);     // pop both s and m
                                return (E) ((mode == REQUEST) ? m.item : s.item);
                            } else                  // lost match
                                s.casNext(m, mn);   // help unlink
                        }
                    }
                } else {                            // help a fulfiller
                    SNode m = h.next;               // m is h's match
                    if (m == null)                  // waiter is gone
                        casHead(h, null);           // pop fulfilling node
                    else {
                        SNode mn = m.next;
                        if (m.tryMatch(h))          // help match
                            casHead(h, mn);         // pop both h and m
                        else                        // lost match
                            h.casNext(m, mn);       // help unlink
                    }
                }
            }
        }

        TransferStack和TransferQueue都是Transfer接口的实现类,这两个类都通过实现transfer()方法达到同步队列的目的。对于TransferStack类,栈中的每个节点包含三种模式:

  • REQUEST:表示节点是一个未完成消费的消费者;
  • DATA:表示节点是一个未完成生产的生产者;
  • FULFILL:表示节点正在执行另一个未完成的生产者或消费者。

        transfer根据传入的参数判断当前调用方法是什么模式。根据这三种模式,transfer方法的处理方式也分为以下三种:

  • 栈顶元素如果明显为空或已经包含相同模式的节点,则尝试push节点到栈中并等待匹配,返回它,如果取消则返回null。
  • 如果明显包含互补模式的节点,则尝试push节点到栈中并与对应的等待节点匹配,并从堆栈中pop出这两个节点,返回匹配的项。由于其他线程可能在执行第三种情况,匹配或取消链接可能没有必要。
  • 如果栈顶已经拥有另一个已完成的节点那么通过执行匹配或pop操作来帮助它出栈,并继续操作。帮助的代码本质上与实际代码相同,只是其不返回值。

        TransferQueue与TransferStack实现类似,只是底层的数据结构为队列。在此不再进行分析,如果感兴趣可以自行阅读源码。

        介绍了Transfer接口与实现,接下来便是 SynchronousQueue的各个方法如何通过Transfer接口实现同步队列。其实SynchronousQueue的方法实现非常简单,不管是入队还是出队,都是调用Transfer接口的transfer方法来完成的。对于入队方法,transfer的第一个参数不为空;而对于出队方法,transfer的第一个参数为空。

6. 延时阻塞队列——DelayQueue

        首先,要使用延时队列,存放的元素必须实现Delayed接口。该接口的定义如下:

public interface Delayed extends Comparable {

    /**
     * Returns the remaining delay associated with this object, in the
     * given time unit.
     *
     * @param unit the time unit
     * @return the remaining delay; zero or negative values indicate
     * that the delay has already elapsed
     */
    long getDelay(TimeUnit unit);
}

        该接口只有一个方法getDeplay,该方法返回给定单位的与该对象关联的剩余延迟时间。

        DeplayQueue队列通过PriorityQueue来实现延时队列,即DeplayQueue会根据元素的延迟时间来排序。

    public boolean offer(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            q.offer(e);
            if (q.peek() == e) {
                leader = null;
                available.signal();
            }
            return true;
        } finally {
            lock.unlock();
        }
    }

        入队时,通过调用PriorityQueue的offer()方法入队,保持队列元素的排列顺序;然后调用peek()方法获取优先队列的队首元素,该元素为延迟时间最小的元素,即即将过期,此时会调用available的signal()方法通知等待的线程有元素可用;然后返回结果。实现较为简单。

    public E poll(long timeout, TimeUnit unit) throws InterruptedException {
        long nanos = unit.toNanos(timeout);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            for (;;) {
                E first = q.peek();
                if (first == null) {
                    if (nanos <= 0)
                        return null;
                    else
                        nanos = available.awaitNanos(nanos);
                } else {
                    long delay = first.getDelay(NANOSECONDS);
                    if (delay <= 0)
                        return q.poll();
                    if (nanos <= 0)
                        return null;
                    first = null; // don't retain ref while waiting
                    if (nanos < delay || leader != null)
                        nanos = available.awaitNanos(nanos);
                    else {
                        Thread thisThread = Thread.currentThread();
                        leader = thisThread;
                        try {
                            long timeLeft = available.awaitNanos(delay);
                            nanos -= delay - timeLeft;
                        } finally {
                            if (leader == thisThread)
                                leader = null;
                        }
                    }
                }
            }
        } finally {
            if (leader == null && q.peek() != null)
                available.signal();
            lock.unlock();
        }
    }

        出队时,不管是非阻塞式方法还是阻塞式方法,即实现都一样。首先,通过peek()方法获取队首元素,如果该元素为空或者该元素的getDelay()方法返回值大于0,即还未到期,则返回空或等待;反之,即该元素的getDelay()方法返回值小于0,则直接调用poll()方法返回该值。

        这里,就介绍了大部分的阻塞队列。而根据第二节中所介绍的,还有LinkedTransferQueue和LinkedBlockingDeque两个队列的实现原理没有介绍。对于LinkedBlockingDeque,其与LinkedBlockingQueue类似,不过LinkedBlockingDeque是一个双向队列,所以在此不再多进行介绍。而LinkedTransferQueue,其实现原理比较复杂,这里也不进行过多介绍,有机会可以在后期单独对其实现原理进行分析。

你可能感兴趣的:(Java,Java,Concurrent)