java 优先级并发队列_Java优先级队列DelayedWorkQueue原理分析

我们知道线程池运行时,会不断从任务队列中获取任务,然后执行任务。如果我们想实现延时或者定时执行任务,重要一点就是任务队列会根据任务延时时间的不同进行排序,延时时间越短地就排在队列的前面,先被获取执行。队列是先进先出的数据结构,就是先进入队列的数据,先被获取。但是有一种特殊的队列叫做优先级队列,它会对插入的数据进行优先级排序,保证优先级越高的数据首先被获取,与数据的插入顺序无关。

实现优先级队列高效常用的一种方式就是使用堆。

一. 用堆实现优先级队列

在常用排序算法总结这篇文章中,我们详细地讲解了堆排序的实现。这里我们回顾一下。

1.1 什么是堆它是一个完全二叉树,即除了最后一层节点不是满的,其他层节点都是满的,即左右节点都有。

它不是二叉搜索树,即左节点的值都比父节点值小,右节点的值都不比父节点值小,这样查找的时候,就可以通过二分的方式,效率是(log N)。

它是特殊的二叉树,它要求父节点的值不能小于子节点的值。这样保证大的值在上面,小的值在下面。所以堆遍历和查找都是低效的,因为我们只知道

从根节点到子叶节点的每条路径都是降序的,但是各个路径之间都是没有联系的,查找一个值时,你不知道应该从左节点查找还是从右节点开始查找。

它可以实现快速的插入和删除,效率都在(log N)左右。所以它可以实现优先级队列。

堆是一个二叉树,但是它最简单的方式是通过数组去实现二叉树,而且因为堆是一个完全二叉树,就不存在数组空间的浪费。怎么使用数组来存储二叉树呢?就是用数组的下标来模拟二叉树的各个节点,比如说根节点就是0,第一层的左节点是1,右节点是2。由此我们可以得出下列公式:// 对于n位置的节点来说:int left = 2 * n + 1; // 左子节点int right = 2 * n + 2; // 右子节点int parent = (n - 1) / 2; // 父节点,当然n要大于0,根节点是没有父节点的

对于堆来说,只有两个操作,插入insert和删除remove,不管插入还是删除保证堆的成立条件,1.是完全二叉树,2.父节点的值不能小于子节点的值。public void insert(int value) {         // 第一步将插入的值,直接放在最后一个位置。并将长度加一

store[size++] = value;         // 得到新插入值所在位置。

int index = size - 1;         while(index > 0) {             // 它的父节点位置坐标

int parentIndex = (index - 1) / 2;             // 如果父节点的值小于子节点的值,你不满足堆的条件,那么就交换值

if (store[index] > store[parentIndex]) {

swap(store, index, parentIndex);

index = parentIndex;

} else {                 // 否则表示这条路径上的值已经满足降序,跳出循环

break;

}

}

}

主要步骤:直接将value插入到size位置,并将size自增,这样store数组中插入一个值了。

要保证从这个叶节点到根节点这条路径上的节点,满足父节点的值不能小于子节点。

通过int parentIndex = (index - 1) / 2得到父节点,如果比父节点值大,那么两者位置的值交换,然后再拿这个父节点和它的父父节点比较。

直到这个节点值比父节点值小,或者这个节点已经是根节点就退出循环。因为我们每次只插入一个值,所以只需要保证新插入位置的叶节点到根节点路径满足堆的条件,因为其他路径没做操作,肯定是满足条件的。第二因为是直接在size位置插入值,所以肯定满足是完全二叉树这个条件。因为每次循环index都是除以2这种倍数递减的方式,所以它最多循环次数是(log N)次。public int remove() {          // 将根的值记录,最后返回

int result = store[0];          // 将最后位置的值放到根节点位置

store[0] = store[--size];          int index = 0;          // 通过循环,保证父节点的值不能小于子节点。

while(true) {              int leftIndex = 2 * index + 1; // 左子节点

int rightIndex = 2 * index + 2; // 右子节点

// leftIndex >= size 表示这个子节点还没有值。

if (leftIndex >= size) break;              int maxIndex = leftIndex;              if (store[leftIndex] 

swap(store, index, maxIndex);

index = maxIndex;

} else {                  break;

}

}          return result;

}

在堆中最大值就在根节点,所以操作步骤:将根节点的值保存到result中。

将最后节点的值移动到根节点,再将长度减一,这样满足堆成立第一个条件,堆是一个完全二叉树。

使用循环,来满足堆成立的第二个条件,父节点的值不能小于子节点的值。

最后返回result。

那么怎么样满足堆的第二个条件呢?因为根点的值现在是新值,那么就有可能比它的子节点小,所以就有可能要进行交换。我们要找出左子节点和右子节点那个值更大,因为这个值可能要和父节点值进行交换,如果它不是较大值的话,它和父节点进行交换之后,就会出现父节点的值小于子节点。

将找到的较大子节点值和父节点值进行比较。

如果父节点的值小于它,那么将父节点和较大子节点值进行交换,然后再比较较大子节点和它的子节点。

如果父节点的值不小于子节点较大值,或者没有子节点(即这个节点已经是叶节点了),就跳出循环。

每次循环我们都是以2的倍数递增,所以它也是最多循环次数是(log N)次。

所以通过堆这种方式可以快速实现优先级队列,它的插入和删除操作的效率都是O(log N)。

二. DelayedWorkQueue类static class DelayedWorkQueue extends AbstractQueue        implements BlockingQueue {

从定义中看出DelayedWorkQueue是一个阻塞队列。

2.1 重要成员属性// 初始时,数组长度大小。

private static final int INITIAL_CAPACITY = 16;        // 使用数组来储存队列中的元素。

private RunnableScheduledFuture>[] queue =            new RunnableScheduledFuture>[INITIAL_CAPACITY];        // 使用lock来保证多线程并发安全问题。

private final ReentrantLock lock = new ReentrantLock();        // 队列中储存元素的大小

private int size = 0;        //特指队列头任务所在线程

private Thread leader = null;

// 当队列头的任务延时时间到了,或者有新的任务变成队列头时,用来唤醒等待线程

private final Condition available = lock.newCondition();

DelayedWorkQueue是用数组来储存队列中的元素,那么我们看看它是怎么实现优先级队列的。

2.2 插入元素排序siftUp方法private void siftUp(int k, RunnableScheduledFuture> key) {            // 当k==0时,就到了堆二叉树的根节点了,跳出循环

while (k > 0) {                // 父节点位置坐标, 相当于(k - 1) / 2

int parent = (k - 1) >>> 1;                // 获取父节点位置元素

RunnableScheduledFuture> e = queue[parent];                // 如果key元素大于父节点位置元素,满足条件,那么跳出循环

// 因为是从小到大排序的。

if (key.compareTo(e) >= 0)                    break;                // 否则就将父节点元素存放到k位置

queue[k] = e;                // 这个只有当元素是ScheduledFutureTask对象实例才有用,用来快速取消任务。

setIndex(e, k);                // 重新赋值k,寻找元素key应该插入到堆二叉树的那个节点

k = parent;

}            // 循环结束,k就是元素key应该插入的节点位置

queue[k] = key;

setIndex(key, k);

}

通过循环,来查找元素key应该插入在堆二叉树那个节点位置,并交互父节点的位置。具体流程在前面已经介绍过了。

2.3 移除元素排序siftDown方法private void siftDown(int k, RunnableScheduledFuture> key) {            int half = size >>> 1;            // 通过循环,保证父节点的值不能小于子节点。

while (k 

int child = (k <

RunnableScheduledFuture> c = queue[child];                // 右子节点, 相当于 (k * 2) + 2

int right = child + 1;                // 如果左子节点元素值大于右子节点元素值,那么右子节点才是较小值的子节点。

// 就要将c与child值重新赋值

if (right  0)

c = queue[child = right];                // 如果父节点元素值小于较小的子节点元素值,那么就跳出循环

if (key.compareTo(c) <= 0)                    break;                // 否则,父节点元素就要和子节点进行交换

queue[k] = c;

setIndex(c, k);

k = child;

}            queue[k] = key;

setIndex(key, k);

}

通过循环,保证父节点的值不能小于子节点。

2.4 插入元素方法public void put(Runnable e) {

offer(e);

}        public boolean add(Runnable e) {            return offer(e);

}        public boolean offer(Runnable e, long timeout, TimeUnit unit) {            return offer(e);

}

我们发现与普通阻塞队列相比,这三个添加方法都是调用offer方法。那是因为它没有队列已满的条件,也就是说可以不断地向DelayedWorkQueue添加元素,当元素个数超过数组长度时,会进行数组扩容。public boolean offer(Runnable x) {            if (x == null)                throw new NullPointerException();

RunnableScheduledFuture> e = (RunnableScheduledFuture>)x;            // 使用lock保证并发操作安全

final ReentrantLock lock = this.lock;

lock.lock();            try {                int i = size;                // 如果要超过数组长度,就要进行数组扩容

if (i >= queue.length)                    // 数组扩容

grow();                // 将队列中元素个数加一

size = i + 1;                // 如果是第一个元素,那么就不需要排序,直接赋值就行了

if (i == 0) {

queue[0] = e;

setIndex(e, 0);

} else {                    // 调用siftUp方法,使插入的元素变得有序。

siftUp(i, e);

}                // 表示新插入的元素是队列头,更换了队列头,

// 那么就要唤醒正在等待获取任务的线程。

if (queue[0] == e) {

leader = null;                    // 唤醒正在等待等待获取任务的线程

available.signal();

}

} finally {

lock.unlock();

}            return true;

}

主要是三步:元素个数超过数组长度,就会调用grow()方法,进行数组扩容。

将新元素e添加到优先级队列中对应的位置,通过siftUp方法,保证按照元素的优先级排序。

如果新插入的元素是队列头,即更换了队列头,那么就要唤醒正在等待获取任务的线程。这些线程可能是因为原队列头元素的延时时间没到,而等待的。

数组扩容方法:private void grow() {            int oldCapacity = queue.length;            // 每次扩容增加原来数组的一半数量。

int newCapacity = oldCapacity + (oldCapacity >> 1); // grow 50%

if (newCapacity 

newCapacity = Integer.MAX_VALUE;            // 使用Arrays.copyOf来复制一个新数组

queue = Arrays.copyOf(queue, newCapacity);

}

2.5 获取队列头元素

2.5.1 立即获取队列头元素public RunnableScheduledFuture> poll() {            final ReentrantLock lock = this.lock;

lock.lock();            try {

RunnableScheduledFuture> first = queue[0];                // 队列头任务是null,或者任务延时时间没有到,都返回null

if (first == null || first.getDelay(NANOSECONDS) > 0)                    return null;                else

// 移除队列头元素

return finishPoll(first);

} finally {

lock.unlock();

}

}

当队列头任务是null,或者任务延时时间没有到,表示这个任务还不能返回,因此直接返回null。否则调用finishPoll方法,移除队列头元素并返回。// 移除队列头元素

private RunnableScheduledFuture> finishPoll(RunnableScheduledFuture> f) {            // 将队列中元素个数减一

int s = --size;            // 获取队列末尾元素x

RunnableScheduledFuture> x = queue[s];            // 原队列末尾元素设置为null

queue[s] = null;            if (s != 0)                // 因为移除了队列头元素,所以进行重新排序。

siftDown(0, x);

setIndex(f, -1);            return f;

}

这个方法与我们在第一节中,介绍堆的删除方法一样。先将队列中元素个数减一。

将原队列末尾元素设置成队列头元素,再将队列末尾元素设置为null。

调用siftDown(0, x)方法,保证按照元素的优先级排序。

2.5.2 等待获取队列头元素public RunnableScheduledFuture> take() throws InterruptedException {            final ReentrantLock lock = this.lock;

lock.lockInterruptibly();            try {                for (;;) {

RunnableScheduledFuture> first = queue[0];                    // 如果没有任务,就让线程在available条件下等待。

if (first == null)

available.await();                    else {                        // 获取任务的剩余延时时间

long delay = first.getDelay(NANOSECONDS);                        // 如果延时时间到了,就返回这个任务,用来执行。

if (delay <= 0)                            return finishPoll(first);                        // 将first设置为null,当线程等待时,不持有first的引用

first = null; // don't retain ref while waiting

// 如果还是原来那个等待队列头任务的线程,

// 说明队列头任务的延时时间还没有到,继续等待。

if (leader != null)

available.await();                        else {                            // 记录一下当前等待队列头任务的线程

Thread thisThread = Thread.currentThread();

leader = thisThread;                            try {                                // 当任务的延时时间到了时,能够自动超时唤醒。

available.awaitNanos(delay);

} finally {                                if (leader == thisThread)

leader = null;

}

}

}

}

} finally {                if (leader == null && queue[0] != null)                    // 唤醒等待任务的线程

available.signal();

lock.unlock();

}

}

如果队列中没有任务,那么就让当前线程在available条件下等待。如果队列头任务的剩余延时时间delay大于0,那么就让当前线程在available条件下等待delay时间。如果队列插入了新的队列头,它的剩余延时时间肯定小于原来队列头的时间,这个时候就要唤醒等待线程,看看它是否能获取任务。

2.5.3 超时等待获取队列头元素public RunnableScheduledFuture> poll(long timeout, TimeUnit unit)            throws InterruptedException {            long nanos = unit.toNanos(timeout);            final ReentrantLock lock = this.lock;

lock.lockInterruptibly();            try {                for (;;) {

RunnableScheduledFuture> first = queue[0];                    // 如果没有任务。

if (first == null) {                        // 超时时间已到,那么就直接返回null

if (nanos <= 0)                            return null;                        else

// 否则就让线程在available条件下等待nanos时间

nanos = available.awaitNanos(nanos);

} else {                        // 获取任务的剩余延时时间

long delay = first.getDelay(NANOSECONDS);                        // 如果延时时间到了,就返回这个任务,用来执行。

if (delay <= 0)                            return finishPoll(first);                        // 如果超时时间已到,那么就直接返回null

if (nanos <= 0)                            return null;                        // 将first设置为null,当线程等待时,不持有first的引用

first = null; // don't retain ref while waiting

// 如果超时时间小于任务的剩余延时时间,那么就有可能获取不到任务。

// 在这里让线程等待超时时间nanos

if (nanos 

nanos = available.awaitNanos(nanos);                        else {

Thread thisThread = Thread.currentThread();

leader = thisThread;                            try {                                // 当任务的延时时间到了时,能够自动超时唤醒。

long timeLeft = available.awaitNanos(delay);                                // 计算剩余的超时时间

nanos -= delay - timeLeft;

} finally {                                if (leader == thisThread)

leader = null;

}

}

}

}

} finally {                if (leader == null && queue[0] != null)                    // 唤醒等待任务的线程

available.signal();

lock.unlock();

}

}

与take方法相比较,就要考虑设置的超时时间,如果超时时间到了,还没有获取到有用任务,那么就返回null。其他的与take方法中逻辑一样。

三. 总结

使用优先级队列DelayedWorkQueue,保证添加到队列中的任务,会按照任务的延时时间进行排序,延时时间少的任务首先被获取。

作者:wo883721

链接:https://www.jianshu.com/p/587901245c95

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值