JDK Source Priority Blocking Queue

Keywords: Java less snapshot JDK

Today we continue to talk about the implementation of blocking queue. Today's protagonist is Priority Blocking Queue. From the naming point of view, it should be orderly. After all, it's priority queue. So what's the actual situation? Let's look at its internal implementation together and explain in advance, because Priority Blocking Queue is involved. When it comes to the use of heap sorting, if you don't understand it clearly, you can refer to my previous instructions on heap sorting.

Preface

JDK version number: 1.8.0_171

Priority BlockingQueue is an infinite capacity blocking queue, of course, ultimately limited by memory. The internal implementation is an array. Continuous growth will lead to OOM. Because of its infinite capacity, there is no blocking in the queuing operation. As long as the memory is enough, the queuing operation thread can join the queue, of course. There is still a need to scramble for mutexes, but there will be no blocking wait operation when the queue is full.

At the same time, although this queue is called priority blocking queue, it will not be sorted immediately after the queue entry operation, and it will be sorted by priority queue only when the queue exit operation or drainTo transfer queue. Priority BlockingQueue is sorted by Comparator, so the incoming object itself has implemented the Comparator interface or passed in a Comparator instance object.

Priority BlockingQueue sorting is achieved through the smallest heap. In previous articles, I have specifically explained the algorithm of heap sorting. I will not elaborate on it here, but I can refer to the explanation of heap sorting first. The initial capacity of priority queue defaults to 11. When the queue space is insufficient, the expansion operation will be carried out. The size of expansion depends on the capacity before expansion.

Class Definition

public class PriorityBlockingQueue<E> extends AbstractQueue<E>
    implements BlockingQueue<E>, java.io.Serializable

Constants/variables

    /**
     * Default initialization array length 11
     */
    private static final int DEFAULT_INITIAL_CAPACITY = 11;

    /**
     * The maximum allowable array length, minus 8, is because it is possible for some virtual opportunities to use part of the space to store object header information.
     */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    /**
     * 
     * Priority queue is realized by balancing binary heap and can be compared with heap sorting algorithm.
     * Then queue[n] corresponds to queue[2*n+1] and [2*(n+1)]
     * Objects in the queue must be comparable, either by default natural sorting or by self-implementing Comparator.
     * If the queue is not empty, queue[0] is the minimum, i.e. sorted by the minimum binary heap.
     */
    private transient Object[] queue;

    /**
     * Number of elements contained in priority queue
     */
    private transient int size;

    /**
     * Comparator object, null when using natural sort comparison
     */
    private transient Comparator<? super E> comparator;

    /**
     * Mutex, only one ReentrantLock
     */
    private final ReentrantLock lock;

    /**
     * Non-null semaphores, queues, space-time blocking out-of-queue threads
     * Just judge if the queue is empty and the queue is not full, so it is an infinite capacity queue.
     */
    private final Condition notEmpty;

    /**
     * Spin Lock, realized by CAS operation
     */
    private transient volatile int allocationSpinLock;

    /**
     * Used in serialization for compatibility with older versions
     */
    private PriorityQueue<E> q;

The allocation SpinLock acquisition of memory offset in the object is implemented in the static code block as follows, which is used in subsequent CAS operations.

    // Unsafe mechanics
    private static final sun.misc.Unsafe UNSAFE;
    private static final long allocationSpinLockOffset;
    static {
        try {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class<?> k = PriorityBlockingQueue.class;
            allocationSpinLockOffset = UNSAFE.objectFieldOffset
                (k.getDeclaredField("allocationSpinLock"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }

Construction method

When no parameters are passed, the default initialization array length is 11, that is, the default capacity of the priority queue is 11. The main reason is that when the set parameters are passed in, it needs to judge whether the conditions used by Priority BlockingQueue are satisfied, that is, non-empty and comparable objects. In addition, it needs to judge whether the heap operation is needed.

    public PriorityBlockingQueue() {
        this(DEFAULT_INITIAL_CAPACITY, null);
    }
    public PriorityBlockingQueue(int initialCapacity) {
        this(initialCapacity, null);
    }
    public PriorityBlockingQueue(int initialCapacity,
                                 Comparator<? super E> comparator) {
        if (initialCapacity < 1)
            throw new IllegalArgumentException();
        this.lock = new ReentrantLock();
        this.notEmpty = lock.newCondition();
        this.comparator = comparator;
        this.queue = new Object[initialCapacity];
    }
    public PriorityBlockingQueue(Collection<? extends E> c) {
        this.lock = new ReentrantLock();
        this.notEmpty = lock.newCondition();
        // Do you need to reorder identifiers, that is, heap identifiers?
        boolean heapify = true; // true if not known to be in heap order
        // Null Value Check Identification
        boolean screen = true;  // true if must screen for nulls
        // If the set is SortedSet, its Comparator sort is used. Because of its existing order, it can be copied directly without heaping operation.
        if (c instanceof SortedSet<?>) {
            SortedSet<? extends E> ss = (SortedSet<? extends E>) c;
            this.comparator = (Comparator<? super E>) ss.comparator();
            heapify = false;
        }
        // If the collection is PriorityBlockingQueue, its Comparator sort is used
        else if (c instanceof PriorityBlockingQueue<?>) {
            PriorityBlockingQueue<? extends E> pq =
                (PriorityBlockingQueue<? extends E>) c;
            this.comparator = (Comparator<? super E>) pq.comparator();
            screen = false;
            // Precise to the PriorityBlockingQueue class, because of its existing order, it can be directly copied without the need for heap operations.
            if (pq.getClass() == PriorityBlockingQueue.class) // exact match
                heapify = false;
        }
        Object[] a = c.toArray();
        int n = a.length;
        // If c.toArray incorrectly doesn't return Object[], copy it.
        // If Object [] is not returned correctly, copy a
        if (a.getClass() != Object[].class)
            a = Arrays.copyOf(a, n, Object[].class);
        // Null Value Check for Sets
        // This. comparator!= null judges the null value of an object of SortedSet type
        // The feeling that n = 1 should be the operation n >= 1 on naturally ordered objects, otherwise, when heapify() heap operations are compared, you can try putting List on your own.
        if (screen && (n == 1 || this.comparator != null)) {
            for (int i = 0; i < n; ++i)
                if (a[i] == null)
                    throw new NullPointerException();
        }
        this.queue = a;
        this.size = n;
        // Heap operation
        if (heapify)
            heapify();
    }

Important methods

tryGrow

The expansion operation is called when the lock is acquired in the offer. The lock is released before expansion. The allocation SpinLock identifier is set to 1 by CAS operation, indicating that the lock is currently being expanded. When expansion is completed, the lock is retrieved, and the allocation SpinLock identifier is set to 0.


    private void tryGrow(Object[] array, int oldCap) {
        // Release the lock first
        lock.unlock(); // must release and then re-acquire main lock
        // Prepared new arrays
        Object[] newArray = null;
        // When other threads do not perform expansion operations, they try to update allocation SpinLock with CAS to identify 1. If successful, the current thread gains expansion operation privileges.
        if (allocationSpinLock == 0 &&
            UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,
                                     0, 1)) {
            try {
                // If the original array capacity is less than 64, each increase of oldCap + 2
                // If the capacity of the original array is greater than or equal to 64, it will increase half of oldCap each time.
                int newCap = oldCap + ((oldCap < 64) ?
                                       (oldCap + 2) : // grow faster if small
                                       (oldCap >> 1));
                // New Array Capacity Over Maximum Array Length Limitation
                if (newCap - MAX_ARRAY_SIZE > 0) {    // possible overflow
                    int minCap = oldCap + 1;
                    // The original array capacity plus 1 has overflowed or thrown OOM directly beyond the maximum length limit
                    if (minCap < 0 || minCap > MAX_ARRAY_SIZE)
                        throw new OutOfMemoryError();
                    // Set the new array capacity to maximum
                    newCap = MAX_ARRAY_SIZE;
                }
                // If the expansion is successful and the current array is not manipulated by other threads, a new array is created.
                if (newCap > oldCap && queue == array)
                    newArray = new Object[newCap];
            } finally {
                // Recovery of Expansion Identification
                allocationSpinLock = 0;
            }
        }
        // Other threads are already expanding to let the cpu go
        if (newArray == null) // back off if another thread is allocating
            Thread.yield();
        // Re-acquisition of locks
        lock.lock();
        // If the expansion is successful and the original array is not operated by other threads, the original array is copied to the new array.
        if (newArray != null && queue == array) {
            queue = newArray;
            System.arraycopy(array, 0, newArray, 0, oldCap);
        }
    }

siftUpComparable/siftUpUsingComparator

Similar to heap sorting operations, the difference is that these methods are analogous to inserting new nodes, that is, when adding new values to the array, the whole heap needs to be adjusted after adding, and Up also shows that heap balance is adjusted from bottom to top. Before invoking this method, the heap should be balanced. If it is unbalanced, the heap operation needs to be done first. Refer to heapify method. When entering the queue, inserting x into the position of k is equivalent to putting new elements into the position of k (not fully executed, need to satisfy the heap characteristics). Since the new elements may damage the whole heap, it is necessary to adjust the whole heap from bottom to top until x is greater than or equal to its parent node or reaches the root node.

    /**
     * @param k the position to fill
     * @param x the item to insert
     * @param array the heap array
     */
    private static <T> void siftUpComparable(int k, T x, Object[] array) {
        Comparable<? super T> key = (Comparable<? super T>) x;
        while (k > 0) {
            // Find the parent node at k
            int parent = (k - 1) >>> 1;
            // Value corresponding to parent node
            Object e = array[parent];
            // Compared with the parent-child node, if the node at k position is larger than or equal to its parent node, it exits and does not need to adjust the heap.
            if (key.compareTo((T) e) >= 0)
                break;
            // If the node at k position is smaller than its parent node, the value at k position is changed to its parent node value.
            array[k] = e;
            // k points to its parent node, which is equivalent to the heap going up a layer, and continues while to determine whether its parent node needs to be adjusted.
            k = parent;
        }
        // At the end of the adjustment, k points to the insertion of the value of x, that is, key.
        array[k] = key;
    }
    
    /**
     * Ibid., the difference is that this uses a comparison object, cmp, with natural sorting above it.
     */
    private static <T> void siftUpUsingComparator(int k, T x, Object[] array,
                                       Comparator<? super T> 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;
    }

siftDownComparable/siftDownUsingComparator

The top element of the stack is array[0]. When the top element of the stack is empty, the element of array[n] is put into position 0 (before it is really executed, it needs to verify whether it meets the heap characteristics first). The translation here is insertion, which can be understood as equivalent to the last element when it is out of the stack. Each leaf node moves to the root (the top of the heap). Here, the whole heap needs to be adjusted from top to bottom to meet the characteristics of the heap. Here, the heap is processed according to the small top heap, while the top one is the minimum, so that the value of the node is less than the value of the two sub-nodes can be satisfied.

    /**
     * @param k the position to fill
     * @param x the item to insert
     * @param array the heap array
     * @param n heap size
     */
    private static <T> void siftDownComparable(int k, T x, Object[] array,
                                               int n) {
        if (n > 0) {
            Comparable<? super T> key = (Comparable<? super T>)x;
            // Final non-leaf node location
            int half = n >>> 1;           // loop while a non-leaf
            while (k < half) {
                // Left child node of k
                int child = (k << 1) + 1; // assume left child is least
                // The value corresponding to the left child node of k
                Object c = array[child];
                // Right child node of k
                int right = child + 1;
                // Maximum value in left and right sub-nodes
                if (right < n &&
                    ((Comparable<? super T>) c).compareTo((T) array[right]) > 0)
                    c = array[child = right];
                // By contrast, if the node value is less than or equal to its left and right sub-nodes, the heap adjustment is completed.
                if (key.compareTo((T) c) <= 0)
                    break;
                // The value at k node is the maximum value in its child node.
                array[k] = c;
                // k points to the index location of the maximum value of its child node
                k = child;
            }
            // At the end of the adjustment, k points to the insertion of the value of x, that is, key.
            array[k] = key;
        }
    }

    /** 
     * Ibid., the difference is that this uses a comparison object, cmp, with natural sorting above it.
     */
    private static <T> void siftDownUsingComparator(int k, T x, Object[] array,
                                                    int n,
                                                    Comparator<? super T> 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;
        }
    }

dequeue

The core operation of queuing requires acquisition of mutex before execution. The element of queuing is array[0] node. After queuing, the operation of heap sorting is carried out. The steps are as follows:

  • Save the array[n] value as x and clear the value at N in the array
  • The siftDownComparable method inserts x into 0 (i.e., the top of the heap) to operate on
  • The siftDownComparable itself goes down to balance the entire heap in turn
  • Heap (array) length - 1

    private E dequeue() {
        // Length of the current queue element
        int n = size - 1;
        // No value returns null
        if (n < 0)
            return null;
        else {
            Object[] array = queue;
            // Save the top element array[0]
            E result = (E) array[0];
            // Save the last element
            E x = (E) array[n];
            // Vacuum clears the last element
            array[n] = null;
            Comparator<? super E> cmp = comparator;
            if (cmp == null)
                // By naturally ordering, the whole heap retains the characteristics of a small top heap, as follows
                siftDownComparable(0, x, array, n);
            else
                // Keep the whole heap small-top heap by sorting incoming comparison class objects
                siftDownUsingComparator(0, x, array, n, cmp);
            size = n;
            return result;
        }
    }

heapify

The heap operation starts at the last non-leaf node and balances each node through the loop until the heap top completes the heap balancing operation.

    /**
     * The constructor is used when passing in a collection
     * Heap the collection to meet the heap's characteristics
     */
    private void heapify() {
        Object[] array = queue;
        int n = size;
        // Last non-leaf node
        int half = (n >>> 1) - 1;
        Comparator<? super E> cmp = comparator;
        if (cmp == null) {
            // Start with the last non-leaf node
            // The siftDownComparable is used here to make the nodes and their children meet the heap characteristics.
            // Step by step, the whole array satisfies the heap characteristics.
            for (int i = half; i >= 0; i--)
                siftDownComparable(i, (E) array[i], array, n);
        }
        else {
            // Ibid., one more comparison object
            for (int i = half; i >= 0; i--)
                siftDownUsingComparator(i, (E) array[i], array, n, cmp);
        }
    }

offer

Inbound operations, which are ultimately called offer, are adjusted from bottom to top using siftUpComparable because we put the new value at the end of the queue and should be adjusted up.


    public boolean offer(E e) {
        if (e == null)
            throw new NullPointerException();
        // Acquire locks
        final ReentrantLock lock = this.lock;
        lock.lock();
        int n, cap;
        Object[] array;
        // Expansion when the array capacity is insufficient, as mentioned above, tryGrow
        while ((n = size) >= (cap = (array = queue).length))
            tryGrow(array, cap);
        try {
            Comparator<? super E> cmp = comparator;
            // Entry operation, putting new nodes in the end, needs to be adjusted from bottom to top using siftUpComparable
            if (cmp == null)
                siftUpComparable(n, e, array);
            else
                siftUpUsingComparator(n, e, array, cmp);
            // Capacity + 1
            size = n + 1;
            // Queuing with data wakes up blocked queuing threads
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
        return true;
    }

poll/take

Out-of-queue operations, which ultimately call dequeue, are similar to the blocking queues mentioned earlier, but with more explanation.

    public E poll() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return dequeue();
        } finally {
            lock.unlock();
        }
    }

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

    public E poll(long timeout, TimeUnit unit) throws InterruptedException {
        long nanos = unit.toNanos(timeout);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        E result;
        try {
            while ( (result = dequeue()) == null && nanos > 0)
                nanos = notEmpty.awaitNanos(nanos);
        } finally {
            lock.unlock();
        }
        return result;
    }

removeAt

Remove an element from the queue, and remove and removeEQ methods also use this method for operations. Here is a place to note that heap adjustments are needed when deleting non-last nodes, adding the last node as a new value to the deletion location, balancing downward through siftDownComparable/siftDownUsingComparator, and calling siftUpComparable/siftUpUsingC if not. Some people may not understand that omparator adjusts upward. In fact, we can understand the characteristics of downheap. It is not completely ordered in the array. In the minimum heap, as long as the parent node is smaller than or equal to its child node, it can be satisfied. So I also explained in the annotation here. If the downheap is adjusted, then the sub-section at I. Point replaces I. The node at I must be larger than the parent node at i, so the child node of I is larger than the parent node at I. There is no need to adjust up. For example, in the case of the following figure, it is necessary to continue to adjust upwards:

After deleting 36 nodes, if 24 nodes are placed on deleted nodes, it will lead to array [i]= move, which needs to be adjusted upward. In fact, it is also because of the characteristics of heap, heap only ensures the orderliness of the top elements, and other elements need to be rebalanced if they are adjusted.

    private void removeAt(int i) {
        Object[] array = queue;
        int n = size - 1;
        // Remove the last node without adjusting the heap
        if (n == i) // removed last element
            array[i] = null;
        else {
            // The following is equivalent to deleting a node in the middle of the queue
            // It's similar to queue operation, but it's not necessarily the top of the heap right now.
            E moved = (E) array[n];
            array[n] = null;
            Comparator<? super E> cmp = comparator;
            if (cmp == null)
                // It is equivalent to putting the node at n into the deleted node position i, and then proceeding downward to the heap balance operation.
                siftDownComparable(i, moved, array, n);
            else
                siftDownUsingComparator(i, moved, array, n, cmp);
            // If there is no downward balancing operation, heap balancing operation should be carried out upwards.
            // If the downward adjustment is made, the child node at I replaces I. The original node at I must be larger than the parent node equal to i, so the child node of I is larger than the parent node equal to I.
            if (array[i] == moved) {
                if (cmp == null)
                    siftUpComparable(i, moved, array);
                else
                    siftUpUsingComparator(i, moved, array, cmp);
            }
        }
        size = n;
    }

drainTo

Transfer maxElements elements to set c. From the source code implementation, we can see that the elements after the transfer are ordered, rather than the arrays in Priority BlockingQueue are disordered. For each transfer, the top elements are added directly, and then the queue operation is performed. Loop calls make the transferred set orderly.

    public int drainTo(Collection<? super E> c, int maxElements) {
        if (c == null)
            throw new NullPointerException();
        if (c == this)
            throw new IllegalArgumentException();
        if (maxElements <= 0)
            return 0;
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            // Set length
            int n = Math.min(size, maxElements);
            for (int i = 0; i < n; i++) {
                // First add
                c.add((E) queue[0]); // In this order, in case add() throws.
                // Out of line operation, heap balanced
                dequeue();
            }
            return n;
        } finally {
            lock.unlock();
        }
    }

iterator

The method used by the iterator is as follows: each call creates a new iterator object Itr, and the entry calls the toArray() method, which copies the current array queue, not the original array queue directly put in.

    public Iterator<E> iterator() {
        return new Itr(toArray());
    }
    public Object[] toArray() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return Arrays.copyOf(queue, size);
        } finally {
            lock.unlock();
        }
    }

Look at its iterator implementation class, which holds a copy of the current array, but the remove operation is the corresponding element in the deleted PrirityBlockingQueue primitive array, which needs attention.

    /**
     * Iterator where the array is a snapshot of the current array
     */
    final class Itr implements Iterator<E> {
        // Array, where you save a snapshot of the original array, refer to the iteration invocation method
        final Object[] array; // Array of all elements
        // Cursor, index of the value corresponding to the next next next execution
        int cursor;           // index of next element to return
        // The index value of the last next element, that is, the index of the value returned by the last next() execution, is -1 if nothing happens.
        int lastRet;          // index of last element, or -1 if no such
        
        // When you get an iterator, call it. Look at the source code at the top.
        Itr(Object[] array) {
            lastRet = -1;
            this.array = array;
        }

        public boolean hasNext() {
            return cursor < array.length;
        }

        public E next() {
            // Cursor pointing over array length, error
            if (cursor >= array.length)
                throw new NoSuchElementException();
            // Update lastRet to record next value index
            lastRet = cursor;
            // Returns the value that next should get, and the cursor index + 1
            return (E)array[cursor++];
        }
            
        public void remove() {
            // Valueless error
            if (lastRet < 0)
                throw new IllegalStateException();
            // This calls the removeEQ method for removal. Note that the corresponding value in the original array of PriorityBlockingQueue is deleted, not the copy array.
            removeEQ(array[lastRet]);
            // Set it to -1
            lastRet = -1;
        }
    }

Iterator is a snapshot version of the original array, so it is out of order. If you want to get an ordered array through the iterator, it is impossible. At the same time, you need to pay attention to remove method to avoid deletion by mistake.

summary

So far, the Priority BlockingQueue source code has been basically explained. What we need to understand is the following points:

  • Priority Blocking Queue is a priority blocking queue with unlimited capacity implemented by an array
  • The default initial capacity is 11, which can be expanded when the capacity is insufficient.
  • Priority Arrangement by Balancing Binary Minimum Heap
  • The set of take, poll method queues or drainTo transfers is orderly

If you have any questions, please point out that the author will revise them in time after verification. Thank you.

Posted by Sturm on Sat, 24 Aug 2019 02:14:46 -0700