PriorityBlockingQueue原理分析

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

属性:

//默认初始化容量大小

private static final int DEFAULT_INITIAL_CAPACITY = 11;

//Object数组,作为队列,假设队列不空的话,代表平衡二叉堆,对于每一个节点n,他的两个孩子为2n+1和2n+2

//优先级队列的排序是按照排序器的,或者按照元素的自然排序

//如果排序器为空,对于每一个节点n,都有他的每一个后代d,n<=d,最小的节点是queue[0]

transient Object[] queue;

//优先级队列的元素个数

private int size = 0;

//比较器,如果优先级队列使用元素的自然排序则为null

private final Comparator<? super E> comparator;

//公共操作使用的锁

private final ReentrantLock lock;

//当队列为空时条件阻塞队列

private final Condition notEmpty;

//cas变量,用来控制同一时间只有一个线程可以扩容

private transient volatile int allocationSpinLock;

构造函数:

第一种:传入一个初始容量和比较器,如果不传,初始容量默认为11,比较器默认为null

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

}

第二种:传入一个集合,将集合中的元素插入到队列中

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();

  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

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

 if (screen && (n == 1 || this.comparator != null)) {

  for (int i = 0; i < n; ++i)

   if (a[i] == null)

    throw new NullPointerException();

 }

 this.queue = a;

 this.size = n;

 if (heapify)

  heapify();

}

主要方法:

offer操作:

如果要插入的元素为空,抛出异常,加锁,保证同一时间只有一个线程可以插入

如果元素个数大于容量值,进行扩容

如果不需要扩容,则拿到比较器,如果为null,则使用元素的自然排序,如果不为null,使用比较器规则来插入元素

然后size+1,因为插入之后至少有一个元素,所以唤醒元素时因为队列为空而进入条件队列的线程,最后释放锁

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);
    
      else
    
       siftUpUsingComparator(n, e, array, cmp);
    
      size = n + 1;
    
      notEmpty.signal();
    
     } finally {
    
      lock.unlock();
    
     }
    
     return true;

}

扩容逻辑:

扩容之前先释放锁,这样是为了扩容时能进行出队、入队、读取操作,提高并发性,

接着对allocationSpinLock这个volatile变量进行cas操作,保证同一时间只有一个线程可以扩容

判断旧容量是否小于64,如果小于64则扩容2倍的旧容量+2,如果不小于64则扩容旧容量的一半,并且最大为MAX_ARRAY_SIZE

扩容完成之后,将allocationSpinLock置为0,注意,这时候还没有将数组替换,所以释放allocationSpinLock锁之后,即使另一个线程去扩容还是扩容一样的大小,就是说虽然会走两遍扩容流程,但是两次扩容下来,大小是一样的,而且替换数组时会判断queue是否与array相等,已经扩容过的情况下不会相等,所以还是只扩容一遍

如果线程没有抢到allocationSpinLock锁,则会让出cpu时间片,但是无法保证,可能扩容线程往下获取锁,可能当前线程获取锁,当前线程获取锁之后会跳出方法,但是扩容的方法是在一个while循环中,进来还是会释放锁,这又给了扩容线程机会

扩容线程获取到锁之后会替换旧数组,并拷贝旧数组

private void tryGrow(Object[] array, int oldCap) {

 lock.unlock(); // must release and then re-acquire main lock//(1)

 Object[] newArray = null;

 if (allocationSpinLock == 0 &&

  UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,

                                   0, 1)) {//(2)

  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;

   }

   if (newCap > oldCap && queue == array)

    newArray = new Object[newCap];

  } finally {

   allocationSpinLock = 0;

  }

 }

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

 }

}

建堆逻辑:

当队列元素个数>0便判断插入位置,第一次拿(队列元素个数-1)/2的位置parent,如果parent位置的元素大于要插入的元素,则将parent位置的元素放入k位置(k为元素个数,作为下标就是前k个元素的下一个位置),

然后将parent的值赋给k,直到找到比要插入的元素小的位置,这就形成了一个最小堆。下面的entity.getYLonCur()方法在比较的时候用了提供的比较器,其他逻辑一样

private static <T> void siftUpComparable(int k, T x, Object[] array) {

 Comparable<? super T> key = (Comparable<? super T>) x;

 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;

}

poll操作:

加锁之后调用dequeue方法

public E poll() {

 final ReentrantLock lock = this.lock;

 lock.lock();

 try {

  return dequeue();

 } finally {

  lock.unlock();

 }

}

dequeue方法:

如果队列为空,直接返回null,拿到队列中的根节点元素,作为结果返回

拿到堆中尾节点的元素,重新构建最小堆或者最大堆

private E dequeue() {

 int n = size - 1;

 if (n < 0)

  return null;

 else {

  Object[] array = queue;

  E result = (E) array[0];

  E x = (E) array[n];

  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;

 }

}

重新构建最小堆的逻辑:

从根节点开始,比较层数为元素个数/2--half,第一次拿出根节点的左右子树中较小的元素,和尾节点元素比较,较小的放到根节点位置,然后从根节点较小的左(left)右(right)子树开始找left或者right的子树中找较小的元素和尾节点元素比较,直到比较层数为half

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;

 }

}

举例:

第一次poll时,拿到根节点元素k=0,key=2作为结果返回,拿到尾节点元素x=11,将尾节点置null,如图(1)
从根节点开始,找到左右子树中较小的元素child=1,c=4,因为4<11,所以根节点变成4,k=child=1,如图(2)
这时k<half=2,继续循环,找到child的左右子树较小的元素child=4,c=8,8<11,所以child位置变成8,k=4,如图(3)
4,这时k>half,结束循环,将k位置变成11,如图(4),并返回

take操作:

先加锁,加锁过程中被其他线程中断会抛出异常

调用dequeue返回根节点元素,如果队列为空,调用notEmpty.await()阻塞自己,知道其他线程调用offer方法,offer方法成功后会调用notEmpty.signal()激活一个阻塞在notEmpty条件队列里的线程

如果队列为空,则会阻塞

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变量的内存可见性

public int size() {

 final ReentrantLock lock = this.lock;

 lock.lock();

 try {

  return size;

 } finally {

  lock.unlock();

 }

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值