PriorityBlockingQueue learning

Disclaimer: This article is a blogger original article, shall not be reproduced without the bloggers allowed. https://blog.csdn.net/ko0491/article/details/90904344

Here Insert Picture Description

PriorityBlockingQueue structure

PriorityB lockingQueue is with unbounded blocking priority queue, each time a team returns the highest priority or
lowest elements. Its interior is achieved using a balanced binary heap, so to traverse the queue element is not directly ensure the orderly. Mo
recognized using the object's compareTo method provides a comparison rules, if you need to customize compare rules you can customize
comparators.

PriorityBlockingQueue 内 部有一个数组 queue,用来存放队列元素,
size 用来存放队列元素个数 。 allocationspi nLock 是个自旋锁,其使用 CAS 操作来保证同
时只有一个线程可以扩容队列,状态为 0 或者 1 ,其中 0 表示当前没有进行扩容, l 表示
当前正在扩容。
由于这是一个优先级队列,所以有一个 比较器 comparator 用来比较元素大小 。 lock 独
占锁对象用来控制同时只能有一个线程可以进行入队、出队操作。 notEmpty 条件变量用
来实现 take 方法阻塞模式。这里没有 notFull 条件变量是因为这里的 put 操作是非阻塞的,
为啥要设计为非阻塞的,是因为这是无界队列。
在如下构造函数中,默认队列容量为 11 ,默认比较器为 null,也就是使用元素的
compareTo 方法进行 比较来确定元素的优先级 , 这意味着队列元素必须实现了 Comparable
接口。

 private static final int DEFAULT_INITIAL_CAPACITY = 11;
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private transient Object[] queue;
private transient Comparator<? super E> comparator;
 private final ReentrantLock lock;
private final Condition notEmpty;
private transient volatile int allocationSpinLock;
public PriorityBlockingQueue() {
        this(DEFAULT_INITIAL_CAPACITY, null);
    }


   public PriorityBlockingQueue(int initialCapacity,
                                 Comparator<? super E> comparator) {
        if (initialCapacity < 1)
            throw new IllegalArgumentException();
        this.lock = new ReentrantLock();
        this.notEmpty = lock.newCondition();
        //比较器默认为null
        this.comparator = comparator;
        this.queue = new Object[initialCapacity];
    }

Here Insert Picture Description

offer操作

offer 操作的作用是在队列中插入一个元素,由于是无界队列 , 所 以 一直返回 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 {
            Comparator<? super E> cmp = comparator;
            //2 默认比较器为 null
            if (cmp == null)
                siftUpComparable(n, e, array);
            else
            //3. 自定义比较器
                siftUpUsingComparator(n, e, array, cmp);
                //将队列元素数增加 1 , 并且激活notEmpty的条件队列里面的 一个阻塞线程
            size = n + 1;
            //激活因调用 take ()方法被阻塞的线程
            notEmpty.signal();
        } finally {
        //解锁
            lock.unlock();
        }
        //返回
        return true;
    }

  • 如何进行扩容和在内部建堆 tryGrow 方法
    private void tryGrow(Object[] array, int oldCap) {
    //释放获取的锁
        lock.unlock(); // must release and then re-acquire main lock
        Object[] newArray = null;
        //CAS成功则扩容
        if (allocationSpinLock == 0 &&
            UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,
                                     0, 1)) {
            try {
            //oldGap<64 则扩容 , 执行oldcap+2 ,否则扩容 50 % ,并且最大为 MAX一皿RAY_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;
            }
        }
        //第 一个线程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);
        }
    }

tryGrow 的作用 是扩容 。 这里为啥在扩容前要先释放锁,然后使用 CAS 控制只有一个
线程可以扩容成功?其实这里不先释放锁,也是可行的,也就是在整个扩容期间一直持有
锁,但是扩容是需要花时间的,如果扩容时还占用锁那么其他线程在这个时候是不能进行
出 队和入队操作的 , 这大大降低 了并发性。 所以为了提高性能 , 使用 CAS 控制只有 一个
线程可 以进行扩容,并且在扩容前释放锁,让其他线程可以进行入队和出队操作.

spinlock 锁使用 CAS 控制只有一 个 线程 可以 进行扩容, CAS 失败的线程会调用
Thread.yield() 让出 CPU , 目的是让扩容线程扩容后优先调用 lock.lock 重新获取锁,但是
这得不到保证。有可能 yield 的线程在扩容线程扩容完成前己经退 出, 并执行代码( 6 )获
取到了锁 , 这时候获取到锁的线程发现 newArray 为 null 就会执行代码 Cl )。如果当前数
组扩容还没完毕 , 当前线程会再次调用 tryGrow 方法 , 然后释放锁 , 这又给扩容线程获取
锁提供了机会 ,如果这时候扩容线程还没扩容完毕 ,则 当 前线程释放锁后又调用 yield 方
法让出 CPU。所以当扩容线程进行扩容时, 其他线程原地自旋通过代码( 1 )检查当前扩
容是否完毕,扩容完毕后才退 出代码 Cl ) 的循环。
扩容线程扩容完毕后会重置自旋锁变量 allocationSpinLock 为 0,这里并没有使
用 UNSAFE 方法的 CAS 进行设置是因为 同时 只可 能 有一个线程获取到该锁 , 并且
alloc ationSpinLock 被修饰为 了 volatile 的。当扩容线程扩容完毕后会执行代码 ( 6 ) 获取锁,
获取锁后复制当前 queue 里面的元素到新数组。

poll操作

poll 操作的作用是获取队列内部堆树的根节点元素,如果队列为空 ,则 返回 null 。


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

    

dequeue

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

如果队列为空则 直接返回 null ,否则执行代码 Cl )获取数组第一个
元素作为返回值存放到变量 Result 中,这里需要注意 ,数组里面的第一个元素是优先级最
小或者最大 的元素 ,出队操作就是返回这个元素。然后代码( 2 )获取队列尾部元素并存
放到变量 x 中 ,且置空尾部节点 ,然后执行代码。 )将变量 x 插入到数组下标为 0 的位置 ,
之后重新调整堆为最大或者最小堆,然后返回 。 这里重要的是,去掉堆的根节点后,如何
使用剩下的节点重新调整一个最大或者最小堆 。 下面我们看下 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;
        }
    }

take操作-可中断

take 操作的作用是获取队列内部堆树的根节点元素 , 如果队列为空则阻塞


public E take() throws InterruptedException {
//获取锁
        final ReentrantLock lock = this.lock;
        //可被中断
        lock.lockInterruptibly();
        E result;
        try {
        //如果队列为空,则!阻塞把当前线程放入notEmpty的条件队 71]
            while ( (result = dequeue()) == null)
            //阻塞当前队列
                notEmpty.await();
        } finally {
            lock.unlock();
        }
        return result;
    }

首先通过 lock. locklnterruptibly() 获取独占锁,以这个方式获取的锁
会对中断进行响应 。然后调用 dequeue 方法返回堆树根节点元素,如果队列为空 ,则返回
fa l se 。然后当前线程调用 notEmpty.await()阻塞挂起自己 , 直到有线程调用了 offer()方法(在
offer 方法内添加元素成功后会调用 notEmp ty.signa l 方法,这会激活一个阻塞在 notEmpty
的条件队列里面的一个线程)。另外,这里使用 while 循环而不是 if 语句是为了避免虚假
唤醒 。

size


    public int size() {
    //加锁
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
        //返回
            return size;
        } finally {
        //释放锁
            lock.unlock();
        }
    }

demo


package com.ghgcn.thread.lesson07.concurrentqueue;

import java.util.Random;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.ThreadLocalRandom;

/**
 * @author 刘楠
 * @since 2019/6/5
 */
public class TestPriorityBlockingQueue {


    static class Task implements Comparable<Task> {
        private Integer priority = 0;
        private String taskName;

        public int compareTo(Task o) {
            if (this.priority >= o.getPriority()) {
                return -1;
            } else {
                return 1;
            }
        }

        public Integer getPriority() {
            return priority;
        }

        public void setPriority(Integer priority) {
            this.priority = priority;
        }

        public String getTaskName() {
            return taskName;
        }

        public void setTaskName(String taskName) {
            this.taskName = taskName;
        }

        public void doSomeThing() {
            System.out.println(taskName + ":" + priority);
        }

    }

    public static void main(String[] args) {

        PriorityBlockingQueue<Task> queue = new PriorityBlockingQueue<Task>();

        ThreadLocalRandom random = ThreadLocalRandom.current();
        for (int i = 0; i < 10; ++i) {
            Task task = new Task();
            task.setPriority(random.nextInt(10));
            task.setTaskName("taskName " + i);
            queue.offer(task);
        }
        //取 出任务执行
        while (!queue.isEmpty()){
            Task task = queue.poll();
            if(null!=task){
                task.doSomeThing();
            }
        }
    }
}

  • 结果

taskName 8:9
taskName 7:9
taskName 0:8
taskName 6:7
taskName 9:6
taskName 3:4
taskName 4:4
taskName 1:4
taskName 5:3
taskName 2:0

From the results, the order of task execution and they are placed in the queue in the order does not matter, but it and
their priorities are.
PriorityBlockingQueue queue using binary heap priority maintenance element, as an element within the array using
the stored data structure, the array is scaleable. The current number of elements> CAS will pass algorithm = maximum capacity
expansion, always ensure that when a team is the team element of the heap root of the tree, rather than the longest stay in the queue inside the meta
element. CompareTo method using elements provide default priority elements of comparison rules, users can customize priority
comparison rules

Guess you like

Origin blog.csdn.net/ko0491/article/details/90904344