阻塞队列:ArrayBlockingQueue和LinkedBlockingQueue(JDK1.8)

线程池中常用的阻塞队列有4种:ArrayBlockingQueue(有限队列)、LinkedBlockingQueue(无限队列)、SynchronousQueue(无空间队列)、DelayedWorkQueue(延迟优先队列)。

ArrayBlockingQueue和LinkedBlockingQueue分别以数组和链表为基础,实现有阻塞功能的队列,较为相似;SynchronousQueue没有实际容量,重点是用来匹配生产者和消费者;DelayedWorkQueue用在ScheduledThreadPoolExecutor中,用来实现延迟优先排序功能。本文重点记录前两个队列的功能和实现,SynchronousQueue和DelayedWorkQueue实现差异较大,在后续文章中记录。

add、offer、put三个方法的区别就不多说了,下文统称为入队,take、poll、remove三个方法也不多说了,下文统称为出队。

ArrayBlockingQueue,在初始化时必须指定容量,容量超过限定时入队失败。

ArrayBlockingQueue内部数组对象是一个整体,内部只有一个锁,由出队和入队公用,所以出队和入队不能同时进行。由两个索引分别指向下一个入队和出队的下标,索引指向数组最大值之后会重置为0,整体实现类似于一个环形队列,在获取锁之后才能够进行入队和出队操作,内部维护一个count来记录队列已满或为空,整体实现并不复杂。

    /** The queued items */
    final Object[] items;

    /** items index for next take, poll, peek or remove */
    int takeIndex;

    /** items index for next put, offer, or add */
    int putIndex;

    /** Number of elements in the queue */
    int count;

    /*
     * Concurrency control uses the classic two-condition algorithm
     * found in any textbook.
     */

    /** Main lock guarding all access */
    final ReentrantLock lock;

    /** Condition for waiting takes */
    private final Condition notEmpty;

    /** Condition for waiting puts */
    private final Condition notFull;

LinkedBlockingQueue为无限队列,初始化时可不指定容量,此时默认容量为Integer.MAX_VALUE(称为无限队列的原因)。

LinkedBlockingQueue内部有两个锁,类似于读锁和写锁分别对入队和出队进行同步("two lock queue" algorithm

),由于链表中的每个节点相对数组更加独立,常用的出入队方法只需要获取其中的一个锁即可进行操作,(count()、contains()、tuArray()等方法需要同时获取读写锁),由于读写分离,LinkedBlockingQueue的吞吐量要大于ArrayBlockingQueue,但是,由于读写锁分离,需要解决读写之间的可见性问题。

     /* Whenever an element is enqueued, the putLock is acquired and
     * count updated.  A subsequent reader guarantees visibility to the
     * enqueued Node by either acquiring the putLock (via fullyLock)
     * or by acquiring the takeLock, and then reading n = count.get();
     * this gives visibility to the first n items.
     * 
     * 当一个元素入队时,需要过去写锁,并更新count,另外一个读线程可以通过获取
     * fullyLock()来获取写锁,获得写入数据的可见性,或者通过读取count值来获取
     * 前n个元素的可见性,
     */

LinkedBlockingQueue的入队方法就是在获取写锁之后,新建节点,加到队尾,有一点需要注意的是,为保证最大限度的吞吐量,只有在写入前锁为空的时候才会去尝试获取读锁,来对读锁上等待的线程进行唤醒:

     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 node = new Node(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)// 队列从0到1的时候会唤醒读线程
            signalNotEmpty();
        return c >= 0;
    }

    private void enqueue(Node node) {
        // assert putLock.isHeldByCurrentThread();
        // assert last.next == null;
        last = last.next = node;
    }

LinkedBlockingQueue出队操作同样在满的队列下出队才会尝试去获取写锁,进行写线程的唤醒,还有一个细节需要注意的是,LinkedBlockingQueue的head节点item值永远为空,出队虽然返回的是head.next的item值,但是删除的是head节点,并重新将head的引用只想head.next这样实现,同样是避免在读写分离的情况下,读写同时操作同一个节点,将新入队的节点挂在了已经删除的节点上。

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

    private E dequeue() {
        // assert takeLock.isHeldByCurrentThread();
        // assert head.item == null;
        Node h = head;
        Node first = h.next;
        h.next = h; // help GC
        head = first;
        E x = first.item;
        first.item = null;
        return x;
    }

LinkedBlockingQueue中还有一个重点remove操作和对GC的支持,源码中解释如下:

    /*
     * To implement weakly consistent iterators, it appears we need to
     * keep all Nodes GC-reachable from a predecessor dequeued Node.
     * That would cause two problems:
     * - allow a rogue Iterator to cause unbounded memory retention
     * - cause cross-generational linking of old Nodes to new Nodes if
     *   a Node was tenured while live, which generational GCs have a
     *   hard time dealing with, causing repeated major collections.
     * However, only non-deleted Nodes need to be reachable from
     * dequeued Nodes, and reachability does not necessarily have to
     * be of the kind understood by the GC.  We use the trick of
     * linking a Node that has just been dequeued to itself.  Such a
     * self-link implicitly means to advance to head.next.
     * 
     * 为了实现弱一致性的迭代器,我们需要对所有节点对已经出队的前任节点
     * 保持GC可达(已经出队的前任节点next扔指向queue中未出队节点)这将
     * 造成两个问题:
     * -允许恶意的Iterator导致无限的内存保留。
     * -如果一个老节点在生命周期内进入老年带,将和一个在年轻带中的新节点
     *  形成跨带引用连接,这会使正常的GC很难应对,并且造成重复的major GC。
     * 然而,已出队的节点仅仅需要未出队节点的地址即可,这种连接并不一定非
     * 要是那种正常的能被GC理解的引用指向的形式,我们将出队的节点next指向他
     * 自己(self-link),当迭代器遇到这样的节点时,就能够理解该节点已经出
     * 队,并且下一个节点应该是head.next。
     */

同时这篇文章的介绍十分清楚:

关于GC的问题

为了实现迭代器的弱一直性,我们不能直接将出队的节点的下一个节点设置为null,因为这会导致迭代器终止,但是这样就给GC带来了难度,假设一个节点X在队列中待的足够久以至于已经进入老年代,然后新创建的位于新生代的Y进入队列即X.Next = Y,这时候即使X出队了但是老年代的Major GC发生的频率较低,导致X暂时驻留老年代,紧接着Y出队,但是由于被X引用导致新生代的minor GC没有回收Y,这时候就出现了所谓的跨代引用,跨代引用造成像Y这样的新生代对象也被复制进入老年代,而且依然不能被老年代的Major GC 回收掉。所以在LinkedBlockingQueue的实现中将出队的节点的下一个节点指向自己来避免这种情况,这就是所谓的self-link。

但是,这只针对正常从头部出队的节点,通过remove的方式移除的节点它的next依然指向了另一个节点,依然会导致跨代引用GC无法回收的问题,因此一定要谨慎使用remove (object)方法。

 

你可能感兴趣的:(JDK源码)