java源码-PriorityBlockingQueue

开篇

 PriorityBlockingQueue是带优先级的无界阻塞队列,每次出队都返回优先级最高的元素是二叉树最小堆的实现。

 使用数组存储的时候i结点的父结点下标就为(i–1)/2。它的左右子结点下标分别为2*i+1和2*i+2

 堆实际上是一棵完全二叉树,其任何一非叶节点满足性质:

  • Key[i]<=key[2i+1]&&Key[i]<=key[2i+2]或者Key[i]>=Key[2i+1]&&key>=key[2i+2],即任何一非叶节点的关键字不大于或者不小于其左右孩子节点的关键字。

  • 堆分为大顶堆和小顶堆,满足Key[i]>=Key[2i+1]&&key>=key[2i+2]称为大顶堆,满足 Key[i]<=key[2i+1]&&Key[i]<=key[2i+2]称为小顶堆。由上述性质可知大顶堆的堆顶的关键字肯定是所有关键字中最大的,小顶堆的堆顶的关键字是所有关键字中最小的,PriorityBlockingQueue是采用小顶堆实现的。


类图

PriorityBlockingQueue.png


PriorityBlockingQueue构造器及相关变量

 PriorityBlockingQueue的相关类变量已经在下面注释了,构造函数核心的参数包括初始化容量大小和比较器comparator
 额外需要关注的是入参为Collection集合对象的时候,内部会区分是否有序,对于有序集合直接添加到数组queue当中,对于无序集合就需要在添加完成后的最后一步执行排序工作。heapify()方法就是执行这个排序的函数,后面请看分解。

public class PriorityBlockingQueue<E> extends AbstractQueue<E>
    implements BlockingQueue<E>, java.io.Serializable {
    private static final long serialVersionUID = 5595510919245408276L;
      
    //初始化容量
    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
    private final Condition notEmpty;

    private transient volatile int allocationSpinLock;


    private PriorityQueue<E> q;

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


heapify过程说明

  heapify的整体逻辑就是一个堆排序过程,排序的对象是数组的0~(n/2-1)之间的元素。整个排序的核心逻辑就是父节点和左右子节点三者进行比较,三者当中最小的元素上浮。这个过程是从(n/2-1)的尾部元素开始到顶部元素进行排序的,所以我们可以理解为先保证底部元素有序后再逐步往顶部走。

    private void heapify() {
        Object[] array = queue;
        int n = size;
        int half = (n >>> 1) - 1;
        Comparator<? super E> cmp = comparator;
        if (cmp == null) {
            for (int i = half; i >= 0; i--)
                siftDownComparable(i, (E) array[i], array, n);
        }
        else {
            for (int i = half; i >= 0; i--)
                siftDownUsingComparator(i, (E) array[i], array, n, cmp);
        }
    }


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


heapify图解说明

针对下面数组说明最小堆的构建过程,初始化状态如下图。
[7, 6, 5, 12, 10, 3, 1, 11, 15, 4 ]

最小堆初始状态

  我们观察下用数组a建成的二叉堆,很明显,对于叶子节点4、15、11、1、3来说它们已经是一个合法的堆(这就是为啥是n/2-1)。所以只要最后一个节点的父节点,也就是最后一个非叶子节点a[4]=10开始调整,然后依次调整a[3]=12,a[2]=5,a[1]=6,a[0]=7,分别对这几个节点做一次"下移或者上浮"操作就可以完成了堆的构造。我们还是用图解来分析下这个过程。

image
image
image

  整个调整过程如下:

    1. 对于节点a[4]=10的调整(图1),只需要交换元素10和其子节点4的位置(图2)。
    1. 对于节点a[3]=12的调整,只需要交换元素12和其最小子节点11的位置(图3)。
    1. 对于节点a[2]=5的调整,只需要交换元素5和其最小子节点1的位置(图4)。
    1. 对于节点a[1]=6的调整,只需要交换元素6和其最小子节点4的位置(图5)。
    1. 对于节点a[0]=7的调整,只需要交换元素7和其最小子节点1的位置,然后交换7和其最小自己点3的位置(图6)。


PriorityBlockingQueue的添加过程

  PriorityBlockingQueue的添加过程就是两个过程

  • 添加元素到数组的最后一个位置
  • 通过siftUpComparable函数实现和父节点进行比较从而实现上浮直至满足最小堆排序。
  • 当然由于PriorityBlockingQueue是线程安全的,所以在底层的添加函数offer当中通过ReentrantLock的lock实现先锁后操作的流程。
    public boolean add(E e) {
        return offer(e);
    }


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

 
    public void put(E e) {
        offer(e); // never need to block
    }

 
    public boolean offer(E e, long timeout, TimeUnit unit) {
        return offer(e); // never need to block
    }

  siftUpComparablem函数的逻辑是递归的比较左字点、右节点、父节点三者之间的关系从而将最小元素进行上浮。

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


PriorityBlockingQueue的添加过程图解

PriorityBlockingQueue的添加过程图解-1
PriorityBlockingQueue的添加过程图解 -2

结合上面的图解,我们来说明一下二叉堆的添加元素过程:

    1. 将元素2添加在最后一个位置(队尾)(图2)。
    1. 由于2比其父亲6要小,所以将元素2上移,交换2和6的位置(图3);
    1. 然后由于2比5小,继续将2上移,交换2和5的位置(图4),此时2大于其父亲(根节点)1,结束。


PriorityBlockingQueue的删除过程

  PriorityBlockingQueue的删除过程就是两个过程:

  • 将需要删除位置的元素和最后子树的元素进行置换并且设置最右子树值为NULL
  • 通过siftDownComparable()方法将待删除位置的新元素进行下沉直至符合最小堆要求
  • 当然由于PriorityBlockingQueue是线程安全的,所以删除操作通过ReentrantLock的lock实现先锁后操作的流程。
public E poll() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return dequeue();
        } finally {
            lock.unlock();
        }
    }

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

    public E poll(long timeout, TimeUnit unit) throws InterruptedException {
        long nanos = unit.toNanos(timeout);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        E result;
        try {
            while ( (result = dequeue()) == null && nanos > 0)
                nanos = notEmpty.awaitNanos(nanos);
        } finally {
            lock.unlock();
        }
        return result;
    }

    public E peek() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return (size == 0) ? null : (E) queue[0];
        } finally {
            lock.unlock();
        }
    }

private void removeAt(int i) {
        Object[] array = queue;
        int n = size - 1;
        if (n == i) // removed last element
            array[i] = null;
        else {
            E moved = (E) array[n];
            array[n] = null;
            Comparator<? super E> cmp = comparator;
            if (cmp == null)
                siftDownComparable(i, moved, array, n);
            else
                siftDownUsingComparator(i, moved, array, n, cmp);
            if (array[i] == moved) {
                if (cmp == null)
                    siftUpComparable(i, moved, array);
                else
                    siftUpUsingComparator(i, moved, array, cmp);
            }
        }
        size = n;
    }

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


PriorityBlockingQueue的删除过程图解

PriorityBlockingQueue的删除过程图解-1
PriorityBlockingQueue的删除过程图解-2
PriorityBlockingQueue的删除过程图解-3

  结合上面的图解,我们来说明一下二叉堆的出队过程:

    1. 将找出队尾的元素8,并将它在队尾位置上删除(图2);
    1. 此时队尾元素8比根元素1的最小孩子3要大,所以将元素1下移,交换1和3的位置(图3);
    1. 然后此时队尾元素8比元素1的最小孩子4要大,继续将1下移,交换1和4的位置(图4);
    1. 然后此时根元素8比元素1的最小孩子9要小,不需要下移,直接将根元素8赋值给此时元素1的位置,1被覆盖则相当于删除(图5),结束。


参考文章

给jdk写注释系列之jdk1.6容器(12)-PriorityQueue源码解析

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,219评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,363评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,933评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,020评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,400评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,640评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,896评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,597评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,327评论 1 244
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,581评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,072评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,399评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,054评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,849评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,672评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,585评论 2 270

推荐阅读更多精彩内容

  • 一些概念 数据结构就是研究数据的逻辑结构和物理结构以及它们之间相互关系,并对这种结构定义相应的运算,而且确保经过这...
    Winterfell_Z阅读 5,531评论 0 13
  • 由于最新项目需要用到图表显示数据,所以参考网上的资源,写了一个demo,主要是通过在Activity发送不同的数据...
    Horrarndoo阅读 1,544评论 1 1
  • 演示模式 实际办法 检查一个对象是否是另一个对象的原型 设置和删除仅影响自身属性 遍历属性 覆写
    鸭梨山大哎阅读 181评论 0 1
  • 牛圈里 两只牛亲吻秀恩爱 即使下一刻烹调成 餐桌上的下酒菜 他们也要谈一场 轰轰烈烈的恋爱 牛郎织女 梁山伯祝英台...
    元气少女a阅读 327评论 7 23
  • “先生,开一瓶宝佳适吗?”你可曾遇到过这样的搭讪? “宝佳适”原是韩国一款功能饮料。但由于近年来,许多老年女性工作...
    IMTVS_cc阅读 36,352评论 0 0