这一篇说一下PriorityBlockingQueue,引用书中的一句话:这就是带优先级的无界阻塞队列,每次出队都返回优先级最高或者最低的元素(这里规则可以自己制定),内部是使用平衡二叉树实现的,遍历不保证有序;

  其实也比较容易,就是基于数组实现的一个平衡二叉树,不了解平衡二叉树的可以先了解一下,别想的太难,原理跟链表差不多,只不过链表中指向下一个节点的只有一个,而平衡二叉树中有两个,一个左,一个右,还有左边的节点的值小于当前节点的值,右边节点的值大于当前节点的值;看看平衡二叉树的增删改查即可;

一.认识PriorityBlockingQueue

  底层是以数组实现的,我们看看几个重要的属性:

  1. //队列默认初始化容量
  2. private static final int DEFAULT_INITIAL_CAPACITY = 11;
  3. //数组最大容量
  4. private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
  5. //底层实现还是数组
  6. private transient Object[] queue;
  7. //队列容量
  8. private transient int size;
  9. //一个比较器,比较元素大小
  10. private transient Comparator<? super E> comparator;
  11. //一个独占锁,控制同时只有一个线程在入队和出队
  12. private final ReentrantLock lock;
  13. //如果队列是空的,还有线程来队列取数据,就阻塞
  14. //这里只有一个条件变量,因为这个队列是无界的,向队列中插入数据的话就用CAS操作就行了
  15. private final Condition notEmpty;
  16. //一个自旋锁,CAS使得同时只有一个线程可以进行扩容,0表示没有进行扩容,1表示正在进行扩容
  17. private transient volatile int allocationSpinLock;

  简单看看构造器:

  1. //默认数组大小是11
  2. public PriorityBlockingQueue() {
  3. this(DEFAULT_INITIAL_CAPACITY, null);
  4. }
  5. //可以指定数组大小
  6. public PriorityBlockingQueue(int initialCapacity) {
  7. this(initialCapacity, null);
  8. }
  9. //初始化数组、锁、条件变量还有比较器
  10. public PriorityBlockingQueue(int initialCapacity,
  11. Comparator<? super E> comparator) {
  12. if (initialCapacity < 1)
  13. throw new IllegalArgumentException();
  14. this.lock = new ReentrantLock();
  15. this.notEmpty = lock.newCondition();
  16. this.comparator = comparator;
  17. this.queue = new Object[initialCapacity];
  18. }
  19. //这个构造器也可以传入一个集合
  20. public PriorityBlockingQueue(Collection<? extends E> c) {
  21. this.lock = new ReentrantLock();
  22. this.notEmpty = lock.newCondition();
  23. boolean heapify = true; // true if not known to be in heap order
  24. boolean screen = true; // true if must screen for nulls
  25. if (c instanceof SortedSet<?>) {
  26. SortedSet<? extends E> ss = (SortedSet<? extends E>) c;
  27. this.comparator = (Comparator<? super E>) ss.comparator();
  28. heapify = false;
  29. }
  30. else if (c instanceof PriorityBlockingQueue<?>) {
  31. PriorityBlockingQueue<? extends E> pq =
  32. (PriorityBlockingQueue<? extends E>) c;
  33. this.comparator = (Comparator<? super E>) pq.comparator();
  34. screen = false;
  35. if (pq.getClass() == PriorityBlockingQueue.class) // exact match
  36. heapify = false;
  37. }
  38. Object[] a = c.toArray();
  39. int n = a.length;
  40. // If c.toArray incorrectly doesn't return Object[], copy it.
  41. if (a.getClass() != Object[].class)
  42. a = Arrays.copyOf(a, n, Object[].class);
  43. if (screen && (n == 1 || this.comparator != null)) {
  44. for (int i = 0; i < n; ++i)
  45. if (a[i] == null)
  46. throw new NullPointerException();
  47. }
  48. this.queue = a;
  49. this.size = n;
  50. if (heapify)
  51. heapify();
  52. }

  有兴趣的可以看看下面这个图,说的更详细,个人觉得看重要的地方就行了;

二.offer方法

  在队列中插入一个元素,由于是无界队列,所以一直返回true;

  1. public boolean offer(E e) {
  2. //如果传入的是null,就抛异常
  3. if (e == null)
  4. throw new NullPointerException();
  5. final ReentrantLock lock = this.lock;
  6. //获取锁
  7. lock.lock();
  8. int n, cap;
  9. Object[] array;
  10. //[1]当前数组中实际数据总数>=数组容量,就进行扩容
  11. while ((n = size) >= (cap = (array = queue).length))
  12. //扩容
  13. tryGrow(array, cap);
  14. try {
  15. Comparator<? super E> cmp = comparator;
  16. //[2]默认比较器为空时
  17. if (cmp == null)
  18. siftUpComparable(n, e, array);
  19. else
  20. //[3]默认比较器不为空就用我们传进去的默认比较器
  21. siftUpUsingComparator(n, e, array, cmp);
  22. //数组实际数量加一
  23. size = n + 1;
  24. //唤醒notEmpty条件队列中的线程
  25. notEmpty.signal();
  26. } finally {
  27. //释放锁
  28. lock.unlock();
  29. }
  30. return true;
  31. }

  上面的代码中,我们就关注那三个地方就行了,首先是[1]中扩容:

  1. private void tryGrow(Object[] array, int oldCap) {
  2. //首先释放获取的锁,这里不释放也行,只是扩容有的时候很慢,需要花时间,此时入队和出队操作就不能进行了,极大地降低了并发性
  3. lock.unlock(); // must release and then re-acquire main lock
  4. Object[] newArray = null;
  5. //自旋锁为0表示队列此时没有进行扩容,然后用CAS将自旋锁从0该为1
  6. if (allocationSpinLock == 0 && UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset, 0, 1)) {
  7. try {
  8. //用这个算法确定扩容后的数组容量,可以看到如果当前数组容量小于64,新数组容量就是2n+2,大于64,新的容量就是3n/2
  9. int newCap = oldCap + ((oldCap < 64) ? (oldCap + 2) : (oldCap >> 1));
  10. //判断新的数组容量是不是超过了最大容量,如果超过了,就尝试在老的数组容量加一,如果还是大于最大容量,就抛异常了
  11. if (newCap - MAX_ARRAY_SIZE > 0) { // possible overflow
  12. int minCap = oldCap + 1;
  13. if (minCap < 0 || minCap > MAX_ARRAY_SIZE)
  14. throw new OutOfMemoryError();
  15. newCap = MAX_ARRAY_SIZE;
  16. }
  17. if (newCap > oldCap && queue == array)
  18. newArray = new Object[newCap];
  19. } finally {
  20. //扩容完毕就将自旋锁变为0
  21. allocationSpinLock = 0;
  22. }
  23. }
  24. //第一个线程在上面的if中执行CAS成功之后,第二个线程就会到这里,然后执行yield方法让出CPU,尽量让第一个线程执行完毕;
  25. if (newArray == null) // back off if another thread is allocating
  26. Thread.yield();
  27. //前面释放锁了,这里要获取锁
  28. lock.lock();
  29. //将原来的数组中的元素复制到新数组中
  30. if (newArray != null && queue == array) {
  31. queue = newArray;
  32. System.arraycopy(array, 0, newArray, 0, oldCap);
  33. }
  34. }

  再看[2]中的默认的比较器:

  1. //这里k表示数组中实际数量,x表示要插入到数组中的数据,array表示存放数据的数组
  2. private static <T> void siftUpComparable(int k, T x, Object[] array) {
  3. //由此可知,我们要放进数组中的数据类型,必须要是实现了Comparable接口的
  4. Comparable<? super T> key = (Comparable<? super T>) x;
  5. //这里判断数组中有没有数据,第一次插入数据的时候,k=0,不满足这个循环条件,那就直接走最下面设置array[0] = key
  6. //满足这个条件的话,首先获取父节点的索引,然后取出值,再比较该值和需要插入值的大小,决定是跳出循环还是继续循环
  7. //这里比较重要,这个循环就是不断的调整二叉树平衡的,下面我们画图看看
  8. while (k > 0) {
  9. int parent = (k - 1) >>> 1;
  10. Object e = array[parent];
  11. if (key.compareTo((T) e) >= 0)
  12. break;
  13. array[k] = e;
  14. k = parent;
  15. }
  16. array[k] = key;
  17. }

  

  随便举个例子看看怎么把平衡二叉树中的元素放到数组中,节点中的数据类型就以Integer了,其实就是将每一层从做到右一次放到数组中存起来,很明显,在数组中不是从小到大的顺序的;

  这里注意一点,平衡二叉树的存放顺序不是唯一的,有很多种情况,跟你的存放顺序有关!

  

  所以我们看看siftUpComparable方法中的while循环是怎么进行的?假设第一次调用offer(3),也就是调用siftUpComparable(0,3,array),这里假设array有足够的大小,不考虑扩容,那么第一次会走到while循环后面执行array[0]=3,下图所示:

  

  第二次调用offer(1),也就是调用siftUpComparable(1,1,array),k=1,parent=0,所以父节点此时应该是3,然后1<3,不满足if语句,设置array[1]=3,k=0,然后继续循环不满足条件,执行array[0]=1,下图所示:

   第三次调用offer(7),也就是调用siftUpComparable(2,7,array),k=2,parent=0,父节点为索引0的位置也就是1,因为7>1满足if语句,所以break跳出循环,执行array[2]=7,下图所示:

  第四次调用offer(2),也就是调用siftUpComparable(3,2,array),k=3,parent=(k-1)>>>1=1,所以父节点表示索引为1的位置,也就是3,因为2<3,不满足if语句,所以设置array[3]=3,k=1,再进行一次循环,parent=0,此时父节点的值是1,2<3,不满足if,设置array[1]=1,k=0;再继续循环不满足循环条件,跳出循环,设置array[0] = 2

  还是很容易的,有兴趣的话再多试试添加几个节点啊!其实还有[3]中使用我们自定义的比较器进行比较,其实i和上面代码一样的,另外put方法就是调用的offer方法,这里就不多说了

三.poll方法

  poll方法的作用是获取并删除队列内部二叉树的根节点,如果队列为空,就返回nul;

  1. public E poll() {
  2. final ReentrantLock lock = this.lock;
  3. //获取独占锁,说明此时不能有其他线程进行入队和出队操作,但是可以进行扩容
  4. lock.lock();
  5. try {
  6. //获取并删除根节点,方法如下
  7. return dequeue();
  8. } finally {
  9. //释放独占锁
  10. lock.unlock();
  11. }
  12. }
  13.  
  14. //这个方法可以好好看看,很有意思
  15. private E dequeue() {
  16. int n = size - 1;
  17. //如果队列为空,就返回null
  18. if (n < 0)
  19. return null;
  20. else {
  21. //否则就先取到数组
  22. Object[] array = queue;
  23. //取到第0个元素,这个也就是要返回的根节点
  24. E result = (E) array[0];
  25. //获取队列实际数量的最后一个元素,并把该位置赋值为null
  26. E x = (E) array[n];
  27. array[n] = null;
  28. Comparator<? super E> cmp = comparator;
  29. if (cmp == null)
  30. //默认的比较器,这里是真正的移除根节点,然后调整在整个平衡二叉树,使得达到平衡
  31. siftDownComparable(0, x, array, n);
  32. else
  33. //我们传入的自定义比较器
  34. siftDownUsingComparator(0, x, array, n, cmp);
  35. //然后数量减一
  36. size = n;
  37. //返回根节点
  38. return result;
  39. }
  40. }
  41.  
  42. private static <T> void siftDownComparable(int k, T x, Object[] array, int n) {
  43. if (n > 0) {
  44. Comparable<? super T> key = (Comparable<? super T>)x;
  45. //[1]
  46. int half = n >>> 1; // loop while a non-leaf
  47. //[2]
  48. while (k < half) {
  49. int child = (k << 1) + 1; // assume left child is least
  50. Object c = array[child];
  51. int right = child + 1;
  52. //[3]
  53. if (right < n &&((Comparable<? super T>) c).compareTo((T) array[right]) > 0)
  54. c = array[child = right];
  55. //[4]
  56. if (key.compareTo((T) c) <= 0)
  57. break;
  58. array[k] = c;
  59. k = child;
  60. }
  61. array[k] = key;
  62. }
  63. }

  所以我们主要的是看看siftDownComparable方法中是怎么将一个去掉了根节点的平衡二叉树调整平衡的;比如现在有如下所示的平衡二叉树:

  调用poll方法,先是把最后一个元素保存起来x=3,然后将最后一个位置设置为null,此时实际调用的是siftDownComparable(0,3,array,3),key=3,half=1,k=0,n=3,满足[2],于是child=1,c=1,right=2,不满足[3],不满足[4],设置array[0]=1,k=1;继续循环,不满足循环条件,跳出循环,直接设置array[1]=3,最后poll方法返回的时2,下图所示:

  其实可以简单的说说,最开始将数组中最后一个值X保存起来在适当时机插入到二叉树中,什么时候是适当时机呢?首先去掉根节点之后,得到根节点左子节点和右子节点的值leftVal和rightVal,如果X比leftVal小,那就直接把X放入到根节点的位置,整个平衡二叉树就平衡了!如果X比leftVal大,那就将leftVal的值设置到根节点中,再以左子节点做递归,继续比较X和左子节点的左节点的大小!仔细看看也没啥。

四.take方法

  这个方法作用是获取二叉树中的根节点,也就是数组的第一个节点,队列为空,就阻塞;

  1. public E take() throws InterruptedException {
  2. //获取锁,可中断
  3. final ReentrantLock lock = this.lock;
  4. lock.lockInterruptibly();
  5. E result;
  6. try {
  7. //如果二叉树为空了,那么dequeue方法就会返回null,然后这里就会阻塞
  8. while ( (result = dequeue()) == null)
  9. notEmpty.await();
  10. } finally {
  11. //释放锁
  12. lock.unlock();
  13. }
  14. return result;
  15. }
  16. //这个方法前面说过,就是删除根节点,然后调整平衡二叉树
  17. private E dequeue() {
  18. int n = size - 1;
  19. if (n < 0)
  20. return null;
  21. else {
  22. Object[] array = queue;
  23. E result = (E) array[0];
  24. E x = (E) array[n];
  25. array[n] = null;
  26. Comparator<? super E> cmp = comparator;
  27. if (cmp == null)
  28. siftDownComparable(0, x, array, n);
  29. else
  30. siftDownUsingComparator(0, x, array, n, cmp);
  31. size = n;
  32. return result;
  33. }
  34. }

五.一个简单的例子

  前面看了这个多方法,那就说说怎么使用吧,看看PriorityBlockingQueue这个阻塞队列怎么使用;

  1. package com.example.demo.study;
  2.  
  3. import java.util.Random;
  4. import java.util.concurrent.PriorityBlockingQueue;
  5.  
  6. import lombok.Data;
  7.  
  8. public class Study0208 {
  9.  
  10. @Data
  11. static class MyTask implements Comparable<MyTask>{
  12. private int priority=0;
  13.  
  14. private String taskName;
  15.  
  16. @Override
  17. public int compareTo(MyTask o) {
  18. if (this.priority>o.getPriority()) {
  19. return 1;
  20. }
  21. return -1;
  22. }
  23. }
  24.  
  25. public static void main(String[] args) {
  26. PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<MyTask>();
  27. Random random = new Random();
  28. //往队列中放是个任务,从TaskName是按照顺序放进去的,优先级是随机的
  29. for (int i = 1; i < 11; i++) {
  30. MyTask task = new MyTask();
  31. task.setPriority(random.nextInt(10));
  32. task.setTaskName("taskName"+i);
  33. queue.offer(task);
  34. }
  35.  
  36. //从队列中取出任务,这里是按照优先级去拿出来的,相当于是根据优先级做了一个排序
  37. while(!queue.isEmpty()) {
  38. MyTask pollTask = queue.poll();
  39. System.out.println(pollTask.toString());
  40. }
  41.  
  42. }
  43.  
  44. }

并发队列之PriorityBlockingQueue的更多相关文章

  1. JAVA多线程(二) 并发队列和阻塞队列

    github代码地址:https://github.com/showkawa/springBoot_2017/tree/master/spb-demo/spb-brian-query-service/ ...

  2. 【Java并发】并发队列与线程池

    并发队列 阻塞队列与非阻塞队 ConcurrentLinkedQueue BlockingQueue ArrayBlockingQueue LinkedBlockingQueue PriorityBl ...

  3. 并发队列之DelayQueue

    已经说了四个并发队列了,DelayQueue这是最后一个,这是一个无界阻塞延迟队列,底层基于前面说过的PriorityBlockingQueue实现的 ,队列中每个元素都有过期时间,当从队列获取元素时 ...

  4. 并发队列 ConcurrentLinkedQueue 及 BlockingQueue 接口实现的四种队列

    队列是一种特殊的线性表,它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作.进行插入操作的端称为队尾,进行删除操作的端称为队头.队列中没有元素时,称为空队列. 在队列这 ...

  5. Java多线程之并发包,并发队列

    目录 1 并发包 1.1同步容器类 1.1.1Vector与ArrayList区别 1.1.2HasTable与HasMap 1.1.3 synchronizedMap 1.1.4 Concurren ...

  6. 解读 java 并发队列 BlockingQueue

    点击添加图片描述(最多60个字)编辑 今天呢!灯塔君跟大家讲: 解读 java 并发队列 BlockingQueue 最近得空,想写篇文章好好说说 java 线程池问题,我相信很多人都一知半解的,包括 ...

  7. 10分钟搞定 Java 并发队列好吗?好的

    | 好看请赞,养成习惯 你有一个思想,我有一个思想,我们交换后,一个人就有两个思想 If you can NOT explain it simply, you do NOT understand it ...

  8. Java并发队列与容器

    [前言:无论是大数据从业人员还是Java从业人员,掌握Java高并发和多线程是必备技能之一.本文主要阐述Java并发包下的阻塞队列和并发容器,其实研读过大数据相关技术如Spark.Storm等源码的, ...

  9. Java并发包源码学习系列:基于CAS非阻塞并发队列ConcurrentLinkedQueue源码解析

    目录 非阻塞并发队列ConcurrentLinkedQueue概述 结构组成 基本不变式 head的不变式与可变式 tail的不变式与可变式 offer操作 源码解析 图解offer操作 JDK1.6 ...

随机推荐

  1. CPP STL学习笔记

    STL的概念 源地址  https://www.ev0l.art/index.php/archives/15/ <li> Iterator (迭代器)<li> Containe ...

  2. 什么是 Google Play服务

    Google Play服务用于更新Google应用和Google Play提供的其他应用. 此组件可提供多种核心功能,例如对您的Google服务进行身份验证.同步联系人信息.提供最新的用户隐私设置,以 ...

  3. Dockerfile文件记录(用于后端项目部署)

    Dockerfile文件记录(用于后端项目部署) 本教程依据个人理解并经过实际验证为正确,特此记录下来,权当笔记. 注:基于linux操作系统(敏感信息都进行了处理) 此文结合另一篇博客共同构成后端服 ...

  4. C# Post发送 接受Xml

    //组合xml内容 StringBuilder strBuilder = new StringBuilder(); var par= @"<xml> <appid>w ...

  5. 【转】推荐!国外程序员整理的Java资源大全

    构建 这里搜集了用来构建应用程序的工具. Apache Maven:Maven使用声明进行构建并进行依赖管理,偏向于使用约定而不是配置进行构建.Maven优于Apache Ant.后者采用了一种过程化 ...

  6. extract函数的使用

    EXTRACT(field FROM source) extract函数从日期/时间数值里抽取子域,比如年.小时等. source必须是一个timestamp, time, interval类型的值表 ...

  7. 你还不会Git?那就不要写代码了(二)

    Git 命令练习 git的删除,添加,修改与日志 which vi 查看命令的目录 ⌃ a 光标去开头 ⌃ E 光标去结尾 ehco 'hellow world asd' > test.txt ...

  8. eclipse反编译插件 jadclipse jad

    eclipse常用反编译插件jadclipse.jad 1.下载jadclipse:net.sf.jadclipse_3.3.0.jar,放到eclipse-plugins 2.下载jad.exe,放 ...

  9. 创建一个区域(Creating an Area) |使用区域 | 高级路由特性 | 精通ASP-NET-MVC-5-弗瑞曼

    摘自:http://www.cnblogs.com/chenboyi081/p/4472709.html#tar2015050302 下面的AdminAreaRegistration继承自AreaRe ...

  10. 关于Xen

    尝试了各种方法搭建xen,尝试过从xenserver入手,但似乎最近时间端不允许访问,感谢我的老师,叫我从kvm入手,暂时记下xen中种种的坑,以后有缘再战.欢迎交流