头图

Unbounded blocking queue PriorityBlockingQueue

introduce

PriorityBlockingQueue is a unbounded blocking queue with priority , and each dequeue returns the element with the highest or lowest priority. Internally, it is implemented using a balanced binary tree heap, so traversal elements are not guaranteed to be ordered.

By default, the compareTo method of the object is used for comparison. If you need to customize the comparison rules, you can customize the comparators.

类图介绍

As can be seen from this class diagram, there is an array queue inside PriorityBlockingQueue, which is used to store queue elements; size is used to store the number of elements; allocationSpinLock is a spin lock, which uses CAS operation to ensure that there is only one thread to expand the queue at the same time, and the state is only 0 and 1, 0 indicates that the expansion is not currently performed, and 1 indicates that the expansion is in progress. Since it is a priority queue, there is a comparator to compare the size, and there is an exclusive lock for lock and a notEmpty condition variable to block the take method. Since is an unbounded queue, there is no notFull condition variable, so put is non-blocking. .

//二叉树最小堆的实现
private transient Object[] queue;
private transient int size;
private transient volatile int allocationSpinLock;
private transient Comparator<? super E> comparator;
private final ReentrantLock lock;
private final Condition notEmpty;

In the constructor, the default queue capacity is 11 , the default comparator is null, that is, the compareTo method of the element is used to determine the priority by default, so the queue element must implement the Comparable interface.

private static final int DEFAULT_INITIAL_CAPACITY = 11;
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];
}

offer action

The role of the offer operation is to insert an element into the queue. Since it is an unbounded queue, it always returns true.

public boolean offer(E e) {
    if (e == null)
        throw new NullPointerException();
    final ReentrantLock lock = this.lock;
    lock.lock();
    int n, cap;
    Object[] array;
    //1. 如果当前元素个数 >= 队列容量 则扩容
    while ((n = size) >= (cap = (array = queue).length))
        tryGrow(array, cap);
    try {
        //2. 默认比较器为null
        Comparator<? super E> cmp = comparator;
        if (cmp == null)
            siftUpComparable(n, e, array);
        else
            //3. 自定义比较器
            siftUpUsingComparator(n, e, array, cmp);
        //4. 队列元素数量增加1,并唤醒notEmpty条件队列中的一个阻塞线程
        size = n + 1;
        notEmpty.signal();
    } finally {
        lock.unlock();
    }
    return true;
}

The above code is not complicated, we mainly look at how to expand and build a heap internally.

Let's look at the expansion logic first:

private void tryGrow(Object[] array, int oldCap) {
        lock.unlock(); // must release and then re-acquire main lock
        Object[] newArray = null;
        //1. CAS成功则扩容
        if (allocationSpinLock == 0 &&
            UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,
                                     0, 1)) {
            try {
                //oldCap<64则扩容执行oldCap+2,否则扩容50%,并且最大值为MAX_ARRAY_SIZE
                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;
            }
        }
       //2. 第一个线程CAS成功后,第二线程进入这段代码,然后第二个线程让出CPU,尽量让第一个线程获取到锁,但得不到保证
        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);
        }
}

The role of tryGrow is to expand, but why release the lock before expanding, and then use CAS to control that only one thread can expand successfully?

In fact, it is ok not to release the lock, that is, the lock is held during the expansion period, but the expansion takes time. If the lock is occupied during this time, other threads cannot perform dequeue and enqueue operations at this time, reducing concurrency. sex. So in order to improve performance, use CAS to control that only one thread can be expanded, and release the lock before expanding, so that other threads can be enqueued and dequeued.

After the expansion thread is expanded, the spin lock variable allocationSpinLock will be reset to 0. The CAS of the UNSAFE method is not used to set it because only one thread can acquire the lock at the same time, and the allocationSpinLock is modified to volatile.

Let's look at the heap building algorithm:

private static <T> void siftUpComparable(int k, T x, Object[] array) {
    Comparable<? super T> key = (Comparable<? super T>) x;
    // 队列元素个数 > 0 则判断插入位置,否则直接入队
    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;
}

If you are familiar with the binary heap, this code is not complicated. Let's look at the specific structure of the following figure:

二叉树

First of all, let's look at parent = (k - 1) >>> 1 , first k - 1 is to get the position of the current real subscript, then >>> 1 gets the position of the parent node, we know in this figure, k = 7 , 06200d893785bf obtained after parent = 3 (k - 1) >>> 1 according to the subscript we know that it is an element 6.

PriorityQueue is a complete binary tree and does not allow null nodes. Its parent nodes are smaller than leaf nodes. This is the minimum heap in heap sorting. The way the binary tree is stored in the array is very simple, that is, from top to bottom, from left to right. A complete binary tree can have a one-to-one correspondence with positions in the array:

  • Left leaf node = parent node subscript * 2 + 1
  • Right leaf node = parent node subscript * 2 + 2
  • parent node = (leaf node - 1) / 2

In fact, it is to compare the element x to be inserted with its parent node element 6, and if it is larger than the parent node, it will always move up.

poll operation

The role of the poll operation is to get the root node element of the heap tree inside the queue, and return null if the queue is empty.

public E poll() {
    final ReentrantLock lock = this.lock;
    //获取独占锁
    lock.lock();
    try {
        return dequeue();
    } finally {
        //释放独占锁
        lock.unlock();
    }
}

We mainly look at the dequeue method.

private E dequeue() {
    int n = size - 1;
    //队列为空,返回null
    if (n < 0)
        return null;
    else {
        Object[] array = queue;
        //1.获取头部元素
        E result = (E) array[0];
        //2. 获取队尾元素,并赋值为null
        E x = (E) array[n];
        array[n] = null;
        Comparator<? super E> cmp = comparator;
        if (cmp == null)//3.
            siftDownComparable(0, x, array, n);
        else
            siftDownUsingComparator(0, x, array, n, cmp);
        size = n; //4.
        return result;
    }
}

This method returns null directly if the queue is empty, otherwise execute code (1) Get the first element of the array as the return value and store it in the variable Result. It should be noted here that the first element in the array has the smallest or largest priority. element, the dequeue operation returns this element. Then code (2) gets the tail element of the queue and stores it in the variable x, and blanks the tail node, and then executes the code (3) inserts the variable x into the position where the subscript of the array is 0, and then readjusts the heap to the maximum or minimum heap , then return. The important thing here is how to use the remaining nodes to resize a max or min heap after removing the root node of the heap. Let's take a look at the implementation of siftDownComparable.

private static <T> void siftDownComparable(int k, T x, Object[] array,
                                           int n) {
    if (n > 0) {
        Comparable<? super T> key = (Comparable<? super T>)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<? 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;
    }
}

Since the 0th element of the queue array is the root, it is removed when dequeuing. At this point the array is no longer the smallest heap, so the heap needs to be adjusted. Specifically, a minimum value is found from the left and right subtrees of the removed tree root to be the tree root, and the left and right subtrees will find the minimum value in their left and right subtrees. This is a recursive process until the leaf node ends the recursion .

Suppose the current queue content is as follows:

初始二叉堆

LeftChildVal = 4; rightChildVal = 6 for the root of the tree in the above figure; since 4 < 6 , so c = 4 . Then since 11 > 4 , which is key > c , element 4 is used to overwrite the value of the root node of the tree.

Then leftChildVal = 8; rightChildVal = 10 in the left and right child nodes of the root of the tree root; since 8 < 10 , so c = 8 . Then because 11 > 8 is key > c , element 8 is used as the root node of the left subtree of the tree root, and the shape of the tree is now as shown in the third step in the following figure. At this time, it is judged whether it is k < half , and the result is false, so the loop is exited. Then set the element of x = 11 to the place where the subscript of the array is 3 . At this time, the heap tree is shown in the fourth step in the figure below, and the heap adjustment is completed.

siftDown之后的二叉堆

put operation

The put operation internally calls the offer operation. Since it is an unbounded queue, it does not need to block.

public void put(E e) {
    offer(e); // never need to block
}

take operation

The role of the take operation is to obtain the root node element of the heap tree inside the queue, and block if the queue is empty.

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

size operation

Get the number of queue elements. The following code adds a lock before returning size to ensure that no other threads are enqueued or dequeued when the size method is called. In addition, since the size variable is not modified as volatile, the locking here also ensures the memory visibility of the size variable under multi-threading.

public int size() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        return size;
    } finally {
        lock.unlock();
    }
}

Summarize

PriorityBlockingQueue is similar to ArrayBlockingQueue and uses an exclusive lock internally to control that only one thread can enqueue and dequeue at the same time. In addition, PriorityBlockingQueue only uses a notEmpty condition variable and does not use notFull. Because it is an unbounded queue, it will never be in the await state when performing a put operation, so it does not need to be woken up. The take method is a blocking method and can be interrupted. This queue is useful when you need to store elements with priority.


神秘杰克
765 声望383 粉丝

Be a good developer.