10-Java多线程-4、并发容器-PriorityBlockingQueue和DelayQueue

并发容器

一、支持优先级的并发容器

PriorityBlockingQueue :支持优先级排序的无界阻塞队列

DelayQueue:使用优先级队列实现的无界阻塞队列。

  • 两者都是继承了AbstractQueue并且实现了BlockingQueue接口;

二、数据结构

2.1 二叉堆

  • 基于数组结构的二进制堆来实现的,接近于二叉树的数据结构,包括大顶堆(节点值大于左右孩子节点)和小顶堆2中形态(节点值小于左右孩子节点)
    二进制堆中父节点和孩子节点的位置关系为:若父节点在n处,那么左孩子节点为:2n+1, 右孩子节点为:2n+2 , 其父节点为(n - 1)/2处。

2.2 插入元素

  • 以大顶堆为例,插入一个元素,如果该元素比父节点小,则不需要移动,如果比父节点大,则一直和父节点交换,直到小于他的父节点,或者自身
    成为根节点为止,复杂度:Ο(logn)

2.3 删除元素

  • 一大顶堆为例,删除第一个元素,然后将队列尾部的元素置于第一个元素的位置,之后将这个元素和孩子节点做比较,(为了便于理解,我们可以认为,
    如果是大顶堆,这个最后的元素一般会相对比较小,被置于第一个位置之后,通常会小于孩子节点),这样会将他和他的孩子节点做交换,直到他大于自己
    的2个孩子节点为止。

三、PriorityBlockingQueue

3.1核心属性:

     // 默认容量
    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 int size;

    // 比较器,如果为空,则为自然顺序
    private transient Comparator<? super E> comparator;

    // 内部锁
    private final ReentrantLock lock;
    //队列为空时阻塞 Condition for blocking when empty
    private final Condition notEmpty;

    //
    private transient volatile int allocationSpinLock;

    // 优先队列:主要用于序列化,这是为了兼容之前的版本。只有在序列化和反序列化才非空
    private PriorityQueue<E> q;
    
    PS:使用单个Condition的原因在于PriorityBlockingQueue是一个无界队列,插入总是会成功,除非消耗尽了资源导致服务器挂。

3.2 构造方法

默认自然排序
//默认是自然排序,可以传入比较对象,默认容量11 (为何默认是11?)
public PriorityBlockingQueue() {
        this(DEFAULT_INITIAL_CAPACITY, null);
    }

3.3 入列操作

add:队尾添加,有返回值,不会阻塞
    public boolean add(E e) {
        return offer(e);
    }
put: 队尾添加,不会阻塞
    public void put(E e) {
        offer(e); // never need to block
    }
offer:队尾添加,队满时返回false,有重载方法支持自定义超时
    public boolean offer(E e) {
            if (e == null)
                throw new NullPointerException(); //参数校验
            final ReentrantLock lock = this.lock;
            lock.lock(); //加锁
            int n, cap;
            Object[] array;
            while ((n = size) >= (cap = (array = queue).length))
                tryGrow(array, cap);  //扩容
            try {
                Comparator<? super E> cmp = comparator; //根据比较器做不同的处理
                if (cmp == null)
                    siftUpComparable(n, e, array);  //comparator为null时自然排序
                else
                    siftUpUsingComparator(n, e, array, cmp);
                size = n + 1;
                notEmpty.signal(); //唤醒消费线程
            } finally {
                lock.unlock();
            }
            return true;
        }
offer超时参数没有意义,无界队列不会阻塞
    public boolean offer(E e, long timeout, TimeUnit unit) {
            return offer(e); // never need to block
        }

3.4 入列参数核心方法(上冒操作)

siftUpComparable:comparator为null时自然排序
        //参数含义分别是:数据元素个数,新加入的元素,二叉堆数组
        private static <T> void siftUpComparable(int k, T x, Object[] array) {
            //如果没有传入比较器,这里来判断新入列的元素应该是Comparable的实现类
            Comparable<? super T> key = (Comparable<? super T>) x; 
            while (k > 0) {
                int parent = (k - 1) >>> 1; //找到父亲节点,父节点下标是:(n-1)/2
                Object e = array[parent];
                if (key.compareTo((T) e) >= 0)  //如果入列的元素大于父亲节点,那么就可以退出循环了,说明已经满足了小顶堆的特性
                    break;
                array[k] = e; //如果父节更大,那就把父节点的元素放到新入列的元素位置
                k = parent;   //再把索引指向父亲节点的位置,便于下一次继续向上查找
            }
            array[k] = key;  //找到了最终需要安放的位置,将入列元素放入
        }
siftUpUsingComparator:comparator不为null时按照Comparator排序
    //参数含义分别是:数据元素个数,新加入的元素,二叉堆数组,比较器
    private static <T> void siftUpUsingComparator(int k, T x, Object[] array, Comparator<? super T> cmp) {
            while (k > 0) {
                int parent = (k - 1) >>> 1; //找到父亲节点,父节点下标是:(n-1)/2
                Object e = array[parent];
                if (cmp.compare(x, (T) e) >= 0) //如果入列的元素大于父亲节点,那么就可以退出循环了,说明已经满足了小顶堆的特性
                    break;
                array[k] = e; //如果父节更大,那就把父节点的元素放到新入列的元素位置
                k = parent; //再把索引指向父亲节点的位置,便于下一次继续向上查找
            }
            array[k] = x; //找到了最终需要安放的位置,将入列元素放入
        }
    PS:siftUpComparable和siftUpUsingComparator方法只有在if判断的时候不一样,其他的地方是一样的,就是有比较器就用比较器比较
    没有比较器就按照自然排序比较

3.5 扩容方法

tryGrow(array, cap)
     private void tryGrow(Object[] array, int oldCap) {
            //只有在offer方法中调用了本方法,进入offer之前就锁了一次,这里先释放锁,因为后面采用的是CAS操作
            lock.unlock(); // must release and then re-acquire main lock
            Object[] newArray = null;
            //采用CAS操作,判断allocationSpinLock是0,并且CAS将其置为1,成功之后进入if逻辑,这里的this参数代表
            //当前对象,allocationSpinLockOffset可以理解为allocationSpinLock的内存地址,然后期望为0,目标修改为1 
            if (allocationSpinLock == 0 && UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset, 0, 1)) {
                try {
                    // 新容量,小于64则变为2倍再加2,大于64则翻三倍
                    int newCap = oldCap + ((oldCap < 64) ?
                                           (oldCap + 2) : // grow faster if small
                                           (oldCap >> 1));
                    //避免溢出越界,最大为MAX_ARRAY_SIZE
                    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;    //将变量改为0
                }
            }
            // 到这里如果是本线程扩容newArray肯定是不为null,为null就是其他线程在处理扩容,那就让给别的线程处理
            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); //最后执行完毕之后回到offer方法的finally块中释放锁
            }
        }
        
        
            /**
             * 用于分配的Spinlock,通过CAS获得。
             * Spinlock for allocation, acquired via CAS.
             */
            private transient volatile int allocationSpinLock;
    

3.6 出列操作

  • PriorityBlockingQueue提供poll()、remove()方法来执行出对操作。出对的永远都是第一个元素:array[0]。
poll方法:队列为空返回null,包含自定义超时的重载方法
        public E poll() {
            final ReentrantLock lock = this.lock;
            lock.lock();
            try {
                return dequeue(); //核心步骤实现
            } finally {
                lock.unlock();
            }
        }
take:队列为空时阻塞
    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;
    }
dequeue方法
       private E dequeue() {
           int n = size - 1;
           if (n < 0)
               return null; //说明size是0,没有元素,返回null
           else {
               Object[] array = queue;
               E result = (E) array[0]; //首个元素,就是要出列的元素
               E x = (E) array[n];  //拿出尾部元素,
               array[n] = null;     //将尾部位置处置为null,因为首元素移除了,尾部元素最终会放到前面的一个位置
               Comparator<? super E> cmp = comparator;
               if (cmp == null)  //没有比较器的处理
                   siftDownComparable(0, x, array, n);
               else         //有比较器的处理
                   siftDownUsingComparator(0, x, array, n, cmp);
               size = n;        //元素个数减1
               return result;  //返回队列首元素
           }
       }
  

3.7 出列参数核心方法(下掉操作)

siftDownComparable:没有比较器时,移除元素后堆的整理方法
        //移除首部元素后需要整理剩下的元素以保持堆的特性
        //参数:新元素插入的位置(移除的总是头部,因此插入位置一直是0),新插入的元素(队尾元素),数组,数组元素个数
        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) {  //k<half说明不是叶子节点,所以才需要处理,如果大于half说明是叶子节点了,
                //那就不需要比了,直接走最后的赋值,放在叶子节点就好了,满足k<half其实说明k节点至少有一个孩子节点
                    //待调整位置k的左节点位置
                    int child = (k << 1) + 1; // assume left child is least
                    Object c = array[child]; //k的左孩子节点
                    int right = child + 1; //k的右孩子节点
                    //如果右孩子没有越界,说明有2个孩子节点,直接和右孩子比较,大于右孩子则把右孩子节点往上放到k节点的位置
                    if (right < n && ((Comparable<? super T>) c).compareTo((T) array[right]) > 0)
                        c = array[child = right];
                    //如果右孩子越界或者比右孩子小,那就和左孩子比较,比较失败说明孩子都比自身大,那就不需要处理啦,直接break
                    //之后,将新的值放在k的位置就可以了
                    if (key.compareTo((T) c) <= 0) 
                        break;
                    //到这里说明有孩子比k节点大,做孩子比k节点小,那么需要将左孩子放到k节点的位置
                    array[k] = c;
                    k = child; //把k指向左孩子,退出循环之后,将新加入的key放在左孩子的地方
                }
                array[k] = key;
            }
        }
        
        上面的逻辑有点绕,其实有是那种情况,一个元素(key)和左右孩子比较,如果小于2个孩子,那就不动,如果大于2个孩子,那就和右孩子
        交换位置,如果大于左孩子小于右孩子,那就和左孩子交换位置。处理思路和二叉堆删除节点的逻辑一样:就第一个元素定义为空穴,然后
        把最后一个元素取出来,尝试插入到空穴位置,并与两个子节点值进行比较,并与其中较小的子节点进行替换,比较的情况就是前面的三种,
        然后继续比较调整。
siftDownUsingComparator:有比较器时,移除元素后堆的整理方法
    和siftDownComparable的逻辑几乎一样,只是比较的使用采用比较器,而不是将元素转换为Comparable对象作比较

3.8 其他

peek:获取不移除
        public E peek() {
            final ReentrantLock lock = this.lock;
            lock.lock();
            try {
                return (size == 0) ? null : (E) queue[0]; //直接返回列头元素,不移除
            } finally {
                lock.unlock();
            }
        }
入列出列对比
  • PriorityBlockingQueue采用二叉堆来维护,所以整个处理过程不是很复杂,添加操作则是不断“上冒”,而删除操作则是不断“下掉”。掌握二
    叉堆就掌握了PriorityBlockingQueue,无论怎么变还是不离其宗。对于PriorityBlockingQueue需要注意的是他是一个无界队列,所以添加操
    作是不会失败的,除非资源耗尽。
其他方法
    还支持其他操作,比如删除指定位置的元素,删除之后也是调用siftDownComparable或者siftDownUsingComparator来调整剩余的数组
    保持小顶堆的性质;

四、DelayQueue

4.1 DelayQueue作用:

    DelayQueue是一个支持延时获取元素的无界阻塞队列。里面的元素全部都是“可延期”的元素,列头的元素是最先“到期”的元素,如
    果队列里面没有元素到期,是不能从列头获取元素的,哪怕有元素也不行。也就是说只有在延迟期到时才能够从队列中取元素。
    DelayQueue主要用于两个方面:
        缓存:清掉缓存中超时的缓存数据
        任务超时处理
    DelayQueue实现的关键主要有如下几个:
        可重入锁ReentrantLock
        用于阻塞和通知的Condition对象
        根据Delay时间排序的优先级队列:PriorityQueue(按照到期的时间为优先级来排序们可以借助PriorityQueue优先级队列来实现)
        用于优化阻塞通知的线程元素leader

4.2 Delayed接口

    public interface Delayed extends Comparable<Delayed> {
    
        /**
         * Returns the remaining delay associated with this object, in the
         * given time unit.
         *
         * @param unit the time unit
         * @return the remaining delay; zero or negative values indicate
         * that the delay has already elapsed
         */
        long getDelay(TimeUnit unit);
    }
    Delayed接口是用来标记那些应该在给定延迟时间之后执行的对象,它定义了一个long getDelay(TimeUnit unit)方法,该方法返回与此
    对象相关的的剩余时间。同时实现该接口的对象必须定义一个compareTo 方法,该方法提供与此接口的getDelay 方法一致的排序。

4.3 核心属性

    /** 可重入锁 */
    private final transient ReentrantLock lock = new ReentrantLock();
     /** 支持优先级的BlockingQueue */
    private final PriorityQueue<E> q = new PriorityQueue<E>();
     /** 用于优化阻塞 */
    private Thread leader = null;
    /** Condition */
    private final Condition available = lock.newCondition();
    
    PS:看了DelayQueue的内部结构就对上面几个关键点一目了然了,但是这里有一点需要注意,DelayQueue的元素都必须继承Delayed接口。
    同时也可以从这里初步理清楚DelayQueue内部实现的机制了:以支持优先级无界队列的PriorityQueue作为一个容器,容器里面的元素都
    应该实现Delayed接口,在每次往优先级队列中添加元素时以元素的过期时间作为排序条件,最先过期的元素放在优先级最高。

4.4 入列操作

offer:队尾添加,队满时返回false,有重载方法支持自定义超时
       public boolean offer(E e) {
           final ReentrantLock lock = this.lock;
           lock.lock();
           try {
                // 向 PriorityQueue中插入元素,其实是使用内部的优先级队列作为容器来保存元素
               q.offer(e);
               // 如果当前元素是队首元素(优先级最高),leader设置为空,唤醒所有等待线程
               if (q.peek() == e) {
                   leader = null;
                   available.signal();
               }
               // 无界队列,永远返回true
               return true;
           } finally {
               lock.unlock();
           }
       }
       
       java.util.PriorityQueue.offer
        public boolean offer(E e) {
               if (e == null)
                   throw new NullPointerException();
               modCount++;
               int i = size;
               if (i >= queue.length)
                   grow(i + 1);
               size = i + 1;
               if (i == 0)
                   queue[0] = e;
               else
                   siftUp(i, e);
               return true;
           }
          
        java.util.PriorityQueue.siftUp
        private void siftUp(int k, E x) {
                if (comparator != null)
                    siftUpUsingComparator(k, x);
                else
                    siftUpComparable(k, x);
            }   
                
        到这里我们明白了,其实内部的优点级队列和PriorityBlockingQueue很类似,也是添加通过维持小堆的特性来保证优先级。
        在PriorityQueue里面是可以兼容包含比较器和不包含比较器2中情况的,因为保存进DelayQueue的元素是必须实现Delay
        这个接口的,因此这样类本身就可以用来做比较,并且比较的就是剩余的到期时间
  

4.5 出列操作

take:
  public E take() throws InterruptedException {
          final ReentrantLock lock = this.lock;
          lock.lockInterruptibly();
          try {
              for (;;) {
                  E first = q.peek(); // 对首元素
                  if (first == null)
                      available.await(); // 对首为空,阻塞,等待offer()操作唤醒
                  else {
                      long delay = first.getDelay(NANOSECONDS); // 获取对首元素的超时时间
                      if (delay <= 0)  // <=0 表示已过期,出队,return
                          return q.poll();
                      first = null; // don't retain ref while waiting
                      if (leader != null)  // leader != null 证明有其他线程在操作,阻塞
                          available.await();
                      else {
                        // 否则将leader 设置为当前线程,独占
                          Thread thisThread = Thread.currentThread();
                          leader = thisThread;
                          try {
                                // 超时阻塞
                              available.awaitNanos(delay);
                          } finally {
                                // 释放leader
                              if (leader == thisThread)
                                  leader = null;
                          }
                      }
                  }
              }
          } finally {
            // 唤醒阻塞线程
              if (leader == null && q.peek() != null)
                  available.signal();
              lock.unlock();
          }
      }
  

五、参考

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值