Read the java.util.concurrent.PriorityBlockingQueue source Note

java.util.concurrent.BlockingQueue

Additional functionality is added to the base of Queue: when traversing a queue, blocking waits if there are no elements, and when inserting elements, waiting for space to be released if there is no additional space.

The methods can be divided into four forms: according to the different results of the same operation (which cannot be satisfied immediately) the following four types are

  1. throw
  2. Returns a specific value, different methods return different values
  3. Block until condition is met
  4. Block for a while and then give up
Summary of BlockingQueue methods
  Throws exception Special value Blocks Times out
Insert {@link #add add(e)} {@link #offer offer(e)} {@link #put put(e)} {@link #offer(Object, long, TimeUnit) offer(e, time, unit)}
Remove {@link #remove remove()} {@link #poll poll()} {@link #take take()} {@link #poll(long, TimeUnit) poll(time, unit)}
Examine {@link #element element()} {@link #peek peek()} not applicable not applicable
  1. Null insertion is not allowed, add\offerput should throw an exception
  2. Originally designed for convenience: the producer-consumer queue
  3. Thread-safe; however, bulk operations, such as addAll containsAll retainAll removeAll s, are not required unless the implementation class is specifically specified.
  4. Memory visibility: the Happen-before principle,

    Memory consistency effects: As with other concurrent
    collections, actions in a thread prior to placing an object into a
    {@code BlockingQueue}
    happen-before
    actions subsequent to the access or removal of that element from
    the {@code BlockingQueue} in another thread.

java.util.concurrent.PriorityBlockingQueue

Blocking Priority Queue

  1. Functionality same as java.util.PriorityQueue
  2. No capacity limit unless you run out of resources and throw OutOfMemory
  3. Null insertion is not allowed
  4. Non-Comparable elements are not allowed to be inserted if the queue depends on the collation of the element itself
  5. Sequence is not guaranteed during iterator traversal.To achieve a consistent order, you need to sort by yourself after toArray and then iterate through it
  6. Elements of the same priority, in irregular order.
  7. Each public method uses an internal lock for concurrency control
  8. All public methods use lock locks for concurrency control
  9. take and poll with timeout are implemented using lock and notEmpty(condition).
  10. In the constructor, locks and notEmpty are initialized, condition s release locks while waiting, and acquire locks again when awakened!
    this.lock = new ReentrantLock();
    this.notEmpty = lock.newCondition();
  1. After acquiring lock, if blocking is supported, lock Interruptibly should be used to acquire the lock!
  2. Four Questions
    1. Why don't you need to re-heap when initializing with SortedSet?
    2. Why was half = (n >>> 1) - 1 specified for initial heap; start?
    3. When siftDown, while's stop condition is k < (n >>>> 1)
    4. Why is array[i] == moved in the removeAt s method last?

Initialize a PriorityBlockingQueue from Collection

    public PriorityBlockingQueue(Collection<? extends E> c) {
        this.lock = new ReentrantLock();
        this.notEmpty = lock.newCondition();
        boolean heapify = true; // true if not known to be in heap order
        boolean screen = true;  // true if must screen for nulls
        if (c instanceof SortedSet<?>) {
            SortedSet<? extends E> ss = (SortedSet<? extends E>) c;
            this.comparator = (Comparator<? super E>) ss.comparator();
            // sorted set is already heaped????In particular, the array returned by the toArray() method
            heapify = false;
        }
        else if (c instanceof PriorityBlockingQueue<?>) {
            PriorityBlockingQueue<? extends E> pq =
                (PriorityBlockingQueue<? extends E>) c;
            this.comparator = (Comparator<? super E>) pq.comparator();
            screen = false;
            if (pq.getClass() == PriorityBlockingQueue.class) // exact match
                // Only the array returned by PriorityBlockingQueue is believed to be heaped.
                heapify = false;
        }
        Object[] a = c.toArray();
        int n = a.length;
        // If c.toArray incorrectly doesn't return Object[], copy it.
        if (a.getClass() != Object[].class)
            a = Arrays.copyOf(a, n, Object[].class);
        // This.comparator!= null:comparator does not allow null
        // n == 1????Compatible with history?
        // The same is true of PriorityQueue.
        if (screen && (n == 1 || this.comparator != null)) {
            for (int i = 0; i < n; ++i)
                if (a[i] == null)
                    //Storage of Null s is not allowed
                    throw new NullPointerException();
        }
        this.queue = a;
        this.size = n;
        if (heapify)
            heapify();
    }

Initialize a PriorityQueue from Collection

    public PriorityQueue(Collection<? extends E> c) {
        if (c instanceof SortedSet<?>) {
            SortedSet<? extends E> ss = (SortedSet<? extends E>) c;
            this.comparator = (Comparator<? super E>) ss.comparator();
            initElementsFromCollection(ss);
        }
        else if (c instanceof PriorityQueue<?>) {
            PriorityQueue<? extends E> pq = (PriorityQueue<? extends E>) c;
            this.comparator = (Comparator<? super E>) pq.comparator();
            initFromPriorityQueue(pq);
        }
        else {
            this.comparator = null;
            initFromCollection(c);
        }
    }
    //Copy the underlying array and size and heap again
    private void initFromCollection(Collection<? extends E> c) {
        initElementsFromCollection(c);
        heapify();
    }

    //No need to re-heap
    private void initElementsFromCollection(Collection<? extends E> c) {
        Object[] a = c.toArray();
        // If c.toArray incorrectly doesn't return Object[], copy it.
        if (a.getClass() != Object[].class)
            a = Arrays.copyOf(a, a.length, Object[].class);
        int len = a.length;
        // Not understood here.Why check empty elements under this condition
        if (len == 1 || this.comparator != null)
            for (int i = 0; i < len; i++)
                if (a[i] == null)
                    throw new NullPointerException();
        this.queue = a;
        this.size = a.length;
    }
    // Reheap is not required only when initialized from another PriorityQueue
    private void initFromPriorityQueue(PriorityQueue<? extends E> c) {
        if (c.getClass() == PriorityQueue.class) {
            this.queue = c.toArray();
            this.size = c.size();
        } else {
            initFromCollection(c);
        }
    }

  1. For the constructor that takes a set as a parameter, there is a question: when the set type is SortedSet, it does not need to be recomposed.Is the storage order of the underlying SortedSet storage structure the same logic as the PrirityQueue?The order of the array returned by toArray meets the requirements of PriortyQueue, meaning that the lowest weight comes first and the positional relationship between parent and child nodes meets the following requirements:

Priority queue represented as a balanced binary heap: the two
children of queue[n] are queue[2n+1] and queue[2(n+1)]. The
priority queue is ordered by comparator, or by the elements'
natural ordering, if comparator is null: For each node n in the
heap and each descendant d of n, n <= d. The element with the
lowest value is in queue[0], assuming the queue is nonempty.

  1. The expansion process when adding elements

Locks are released during the allocation space phase of capacity expansion and reacquired after allocation is complete.Concurrency control is also required when allocating space, and in order not to affect other operations, UNSAFE is used here to implement concurrency control.

Introduction to UNSAFE


    /*
     * The implementation uses an array-based binary heap, with public
     * operations protected with a single lock. However, allocation
     * during resizing uses a simple spinlock (used only while not
     * holding main lock) in order to allow takes to operate
     * concurrently with allocation.  This avoids repeated
     * postponement of waiting consumers and consequent element
     * build-up. The need to back away from lock during allocation
     * makes it impossible to simply wrap delegated
     * java.util.PriorityQueue operations within a lock, as was done
     * in a previous version of this class. To maintain
     * interoperability, a plain PriorityQueue is still used during
     * serialization, which maintains compatibility at the expense of
     * transiently doubling overhead.
     * This queue implementation uses an array-based binary heap and uses a single lock to protect common concurrent operations!However, during the expansion process
     * A simple spin lock (only used when no primary lock is acquired) is used to allow take concurrent operations during expansion.
     * This avoids duplicate delayed waiting for consumers and subsequent element creation.During expansion, the action of exiting the lock can no longer be simple to package and delegate to when the lock is acquired
     * java.util.PriorityQueue Operation, which is also the implementation of the previous version.For compatibility, a simple PriorityQueue is still used in serialization while maintaining
     * Compatibility and temporary doubling of overhead!
     */
    private void tryGrow(Object[] array, int oldCap) {
        lock.unlock(); // must release and then re-acquire main lock
        Object[] newArray = null;
        /**
        * // User comparison and replacement values for this method
          // The first parameter is the object itself to be replaced, and the second parameter is the memory address of the value
          // The third parameter is the expected value of the variable, and the fourth parameter is the value to be converted by the variable
          // If the current value of the variable equals the expected value (the third parameter), the value of the variable is replaced by a new value (the fourth parameter), returning true
          // If it is not equal to the expectation, it does not change and returns false
        *
        */
        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;
                }
                //queue may have changed
                if (newCap > oldCap && queue == array)
                    newArray = new Object[newCap];
            } finally {
                allocationSpinLock = 0;
            }
        }
        // Release resources; reacquire CPU resources
        if (newArray == null) // back off if another thread is allocating
            Thread.yield();
        lock.lock();
        //Check queue == array again, queue must be up to date
        if (newArray != null && queue == array) {
            queue = newArray;
            System.arraycopy(array, 0, newArray, 0, oldCap);
        }
    }

    public boolean offer(E e) {
        if (e == null)
            throw new NullPointerException();
        final ReentrantLock lock = this.lock;
        lock.lock();
        int n, cap;
        Object[] array;
        // Expansion
        while ((n = size) >= (cap = (array = queue).length))
            tryGrow(array, cap);
        try {
            Comparator<? super E> 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;
    }    
  1. Balance heaps in two ways for different scenarios!

When queued, the head node is removed and the root node is empty. The last element is placed at the root node and keeps sinking until it is smaller or equal to its children or becomes a leaf.
Child nodes;
When joining the queue, insert the element directly behind the last element, and keep comparing it with the parent node, bubbling until it is larger or equal to its parent or becomes the root node.
In both methods, the location of the parent or child node needs to be obtained based on the current node.
Child Node of Current Node n: 2n+1,2n+2
Parent of current node n: (n-1)/2

  1. Initialize the process of building a binary heap
    //Initialize heap
    private void heapify() {
        Object[] array = queue;
        int n = size;
        // Why start here?What does it mean?
        int half = (n >>> 1) - 1;
        Comparator<? super E> cmp = comparator;
        if (cmp == null) {
            for (int i = half; i >= 0; i--)
                siftDownComparable(i, (E) array[i], array, n);
        }
        else {
            for (int i = half; i >= 0; i--)
                siftDownUsingComparator(i, (E) array[i], array, n, cmp);
        }
    }
    //Insert element x into position k of array array with array size n
    private static <T> void siftDownComparable(int k, T x, Object[] array,
                                               int n) {
        if (n > 0) {
            Comparable<? super T> key = (Comparable<? super T>)x;
            //Be guaranteed to be non-leaf nodes and stop looping if it is leaf nodes
            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<? super T>) 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;
        }
    }
  1. take method, blocked without elements; use lock and lock.condition to block!
    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        //You can be interrupted, either to acquire a lock or to interrupt; otherwise, you will remain dormant waiting to acquire a lock
        lock.lockInterruptibly();
        E result;
        try {
            while ( (result = dequeue()) == null)
                // Release the lock resource and reacquire the lock when awakened.Interrupted or signaled, otherwise waiting
                //signal, signAll, interupte, spurious wakeup
                notEmpty.await();
        } finally {
            lock.unlock();
        }
        return result;
    }

  1. There are two ways to poll, the difference is whether null is returned.A poll with an empty parameter that immediately returns null when the queue is empty; polls with timeout support allow waiting for timeout.
    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;
    }
  1. Remove element at i position
    /**
     * Removes the ith element from queue.
     */
    private void removeAt(int i) {
        Object[] array = queue;
        int n = size - 1;
        if (n == i) // removed last element
            array[i] = null;
        else {
            E moved = (E) array[n];
            array[n] = null;
            Comparator<? super E> cmp = comparator;
            if (cmp == null)
                siftDownComparable(i, moved, array, n);
            else
                siftDownUsingComparator(i, moved, array, n, cmp);
            //Same node pointer???
            if (array[i] == moved) {
                if (cmp == null)
                    siftUpComparable(i, moved, array);
                else
                    siftUpUsingComparator(i, moved, array, cmp);
            }
        }
        size = n;
    }

  1. Iterator method, traversal is a backup of the traversed array; however, when an element is removed, the element in the true array is removed.The iterator's own copy of the array does not delete elements.

Tags: Programming Java

Posted on Tue, 05 May 2020 03:29:54 -0400 by ochi