PriorityBlockingQueue学习

PriorityBlockingQueue学习_第1张图片

PriorityBlockingQueue结构

PriorityB lockingQueue 是带优先级的无界阻塞队列,每次出队都返回优先级最高或者
最低的元素。其内部是使用平衡二叉树堆实现的,所以直接遍历队列元素不保证有序。默
认使用对象的 compareTo 方法提供比较规则,如果你需要自定义比较规则则可以自定义
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];
    }

PriorityBlockingQueue学习_第2张图片

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

从结果可知 ,任务执行的先后顺序和它们被放入队列的先后顺序没有关系,而是和它
们的优先级有关系。
PriorityBlockingQueue 队列 在内部使用二叉树堆维护元素优先级,使用数组作为元素
存储的数据结构,这个数组是可扩容的 。当前元素个数>=最大容量时会通过 CAS 算法
扩容,出队时始终保证出队的元素是堆树的根节点,而不是在队列里面停留时 间最长的元
素。使用元素的 compareTo 方法提供默认的元素优先级比较规则,用户可以自定义优先级
的比较规则

你可能感兴趣的:(JAVA并发)