【阻塞队列】阻塞队列DelayedWorkQueue源码详解

news2024/11/18 20:28:05

目录

一、前言

二、ScheduledThreadPoolExecutor线程池

三、DelayedWorkQueue延迟阻塞队列

四、工作原理

五、源码分析

5.1 定义

5.2 成员属性

5.3 构造函数

5.4 入队方法

5.4.1 offer添加元素

5.4.2 扩容grow()

5.4.3 向上堆化siftUp

5.5 出队方法

5.5.1 take() 消费元素

5.5.2 finishPoll() 出队列

5.5.3 向下堆化siftDown

5.5.4 poll()

5.5.5 poll(long timeout, TimeUnit unit)

5.5.6 remove() 删除指定元素

六、总结


一、前言

线程池运行时,会不断从任务队列中获取任务,然后执行任务。如果我们想实现延时或者定时执行任务,重要一点就是任务队列会根据任务延时时间的不同进行排序,延时时间越短的就排在队列的前面,先被获取执行。

队列是先进先出的数据结构,就是先进入队列的数据,先被获取。但是有一种特殊的队列叫做优先级队列,它会对插入的数据进行优先级排序,保证优先级越高的数据首先被获取,与数据的插入顺序无关。

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

二、ScheduledThreadPoolExecutor线程池

ScheduledThreadPoolExecutor继承自ThreadPoolExecutor,所以其内部的数据结构和ThreadPoolExecutor基本一样,并在其基础上增加了按时间调度执行任务的功能,分为延迟执行任务周期性执行任务

ScheduledThreadPoolExecutor的构造函数只能传3个参数corePoolSize、ThreadFactory、RejectedExecutionHandler,默认maximumPoolSize为Integer.MAX_VALUE。

工作队列是高度定制化的延迟阻塞队列DelayedWorkQueue,其实现原理和DelayQueue基本一样,核心数据结构是二叉最小堆的优先队列,队列满时会自动扩容,所以offer操作永远不会阻塞,maximumPoolSize也就用不上了,所以线程池中永远会保持至多有corePoolSize个工作线程正在运行。

public ScheduledThreadPoolExecutor(int corePoolSize,
                                   ThreadFactory threadFactory,
                                   RejectedExecutionHandler handler) {
    // 调用父类ThreadPoolExecutor的构造方法来创建定时任务线程池
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue(), threadFactory, handler); // 传入DelayedWorkQueue阻塞队列
}

三、DelayedWorkQueue延迟阻塞队列

DelayedWorkQueue 也是一种设计为定时任务的延迟队列,它的实现和DelayQueue一样,不过是将优先级队列和DelayQueue的实现过程迁移到本身方法体中,从而可以在该过程当中灵活的加入定时任务特有的方法调用。

四、工作原理

ScheduledThreadPoolExecutor之所以要自己实现阻塞的工作队列,是因为 ScheduleThreadPoolExecutor 要求的工作队列有些特殊。

DelayedWorkQueue是一个基于堆的数据结构,类似于DelayQueue和PriorityQueue。在执行定时任务的时候,每个任务的执行时间都不同,所以DelayedWorkQueue的工作就是按照执行时间的升序来排列,执行时间距离当前时间越近的任务在队列的前面(注意:这里的顺序并不是绝对的,堆中的排序只保证了子节点的下次执行时间要比父节点的下次执行时间要大,而叶子节点之间并不一定是顺序的)。

堆结构如下图:

可见,DelayedWorkQueue是一个基于最小堆结构的队列。堆结构可以使用数组表示,可以转换成如下的数组:

在这种结构中,可以发现有如下特性: 假设“第一个元素” 在数组中的索引为 0 的话,则父结点和子结点的位置关系如下:

  • 索引为  的左孩子的索引是 (2 * i + 1);
  • 索引为  的右孩子的索引是 (2 * i + 2);
  • 索引为  的父结点的索引是 floor((i−1)/2);

为什么要使用DelayedWorkQueue呢?

  • 定时任务执行时需要取出最近要执行的任务,所以任务在队列中每次出队时一定要是当前队列中执行时间最靠前的,所以自然要使用优先级队列。
  • DelayedWorkQueue是一个优先级队列,它可以保证每次出队的任务都是当前队列中执行时间最靠前的,由于它是基于堆结构的队列,堆结构在执行插入和删除操作时的最坏时间复杂度是 O(logN)。

五、源码分析

5.1 定义

static class DelayedWorkQueue extends AbstractQueue<Runnable>
        implements BlockingQueue<Runnable> {
 
        /*
         * A DelayedWorkQueue is based on a heap-based data structure
         * like those in DelayQueue and PriorityQueue, except that
         * every ScheduledFutureTask also records its index into the
         * heap array. This eliminates the need to find a task upon
         * cancellation, greatly speeding up removal (down from O(n)
         * to O(log n)), and reducing garbage retention that would
         * otherwise occur by waiting for the element to rise to top
         * before clearing. But because the queue may also hold
         * RunnableScheduledFutures that are not ScheduledFutureTasks,
         * we are not guaranteed to have such indices available, in
         * which case we fall back to linear search. (We expect that
         * most tasks will not be decorated, and that the faster cases
         * will be much more common.)
         *
         * All heap operations must record index changes -- mainly
         * within siftUp and siftDown. Upon removal, a task's
         * heapIndex is set to -1. Note that ScheduledFutureTasks can
         * appear at most once in the queue (this need not be true for
         * other kinds of tasks or work queues), so are uniquely
         * identified by heapIndex.
         */
 
 
        /* 借助Google翻译:
        DelayedWorkQueue基于堆的数据结构,如DelayQueue和PriorityQueue中的数据结构,除了每个
        ScheduledFutureTask还将其索引记录到堆数组中。这消除了在取消时找到任务的需要,大大加快了移除(从
        O(n)到O(log n)),并减少了垃圾保留,否则通过等待元素在清除之前升至顶部而发生垃圾保留。但是因为
        队列也可能包含不是ScheduledFutureTasks的RunnableScheduledFutures,所以我们不能保证有这样的索引可
        用,在这种情况下我们会回到线性搜索。 (我们希望大多数任务都不会被装饰,而且更快的情况会更常见。)
        所有堆操作都必须记录索引更改 - 主要在siftUp和siftDown中。删除后,任务的heapIndex设置为-1。请注
        意,ScheduledFutureTasks最多可以出现在队列中一次(对于其他类型的任务或工作队列,这不一定是这样),
        因此由heapIndex唯一标识。
*/

DelayedWorkQueue 的类继承关系如下:

其包含的方法定义如下:

5.2 成员属性

// 初始时,数组长度大小。
private static final int INITIAL_CAPACITY = 16;        
// 使用数组来储存队列中的元素,根据初始容量创建RunnableScheduledFuture类型的数组
private RunnableScheduledFuture<?>[] queue =  new RunnableScheduledFuture<?>[INITIAL_CAPACITY];        
// 使用ReentrantLock来保证多线程并发安全问题。
private final ReentrantLock lock = new ReentrantLock();        
// 队列中储存元素的大小
private int size = 0;        
// 特指队列头任务所在leader线程。也就是等待要去执行队列头任务的线程
private Thread leader = null;        
// 当队列头的任务延时时间到了,或者新线程可能需要成为leader,用来唤醒等待线程
private final Condition available = lock.newCondition();

DelayedWorkQueue是用数组来储存队列中的元素,数组类型为RunnableScheduledFuture,其实就类似于Runnable,只不过加了一些定时任务的属性,核心数据结构是二叉最小堆的优先队列,队列满时会自动扩容。

数组类型是一个接口,本质的实现还是Runnable:

public interface RunnableScheduledFuture<V> extends RunnableFuture<V>, ScheduledFuture<V> {
    // 该任务是否是周期性的
    boolean isPeriodic();
}

注意这里的leader,它是Leader-Follower模式的变体,用于减少不必要的定时等待。这什么意思呢?

对于多线程的网络模型来说:所有线程会有三种身份中的一种:leader和follower,以及一个干活中的状态:proccesser。它的基本原则就是,永远最多只有一个leader。而所有follower都在等待成为leader。线程池启动时会自动产生一个Leader负责等待网络IO事件,当有一个事件产生时,Leader线程首先通知一个Follower线程将其提拔为新的Leader,然后自己就去干活了,去处理这个网络事件,处理完毕后加入Follower线程等待队列,等待下次成为Leader。这种方法可以增强CPU高速缓存相似性,及消除动态内存分配和线程间的数据交换。

5.3 构造函数

DelayedWorkQueue 是 ScheduledThreadPoolExecutor 的静态类部类,默认只有一个无参构造方法。

static class DelayedWorkQueue extends AbstractQueue<Runnable>
        implements BlockingQueue<Runnable> {
    // ...
}

5.4 入队方法

public void put(Runnable e) {
    offer(e);
}     
   
public boolean add(Runnable e) {            
    return offer(e);
}  
 
// timeout和unit两个参数并没有用
public boolean offer(Runnable e, long timeout, TimeUnit unit) {            
    return offer(e);
}

DelayedWorkQueue 提供了三个插入元素方法:

  • put(e)
  • add(e)
  • offer(e, t, u)

通过源码我们发现与普通阻塞队列相比,这三个添加方法都是调用offer方法。那是因为它没有队列已满的条件,也就是说可以不断地向DelayedWorkQueue添加元素,当元素个数超过数组长度时,会进行数组扩容。

5.4.1 offer添加元素

ScheduledThreadPoolExecutor提交任务时调用的是DelayedWorkQueue.add,而add、put等一些对外提供的添加元素的方法都调用了offer。

public boolean offer(Runnable x) {            
    if (x == null)                
        throw new NullPointerException();
       // 将要加入队列的任务转换为RunnableScheduledFuture类型,因为存储任务的堆数组就是这个类型
    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();
    }      
    // 成功入队则返回true      
    return true;
}

其基本流程如下:

  1. 其作为生产者的入口,首先获取锁。
  2. 判断队列是否要满了(size >= queue.length),满了就扩容grow()。
  3. 队列未满,size+1。
  4. 判断添加的元素是否是队列中的第一个元素,是则不需要堆化。
  5. 添加的元素不是队列中第一个元素,则需要堆化siftUp。
  6. 如果堆顶元素刚好是此时被添加的元素,则唤醒take线程来消费队列中的任务。
  7. 最终释放锁。

offer基本流程图如下:

5.4.2 扩容grow()

可以看到,当队列满时,不会阻塞等待,而是继续扩容。新容量newCapacity在旧容量oldCapacity的基础上扩容50%(oldCapacity >> 1相当于oldCapacity /2)。最后Arrays.copyOf(将旧数组拷贝到一块新的数组空间中),Arrays.copyOf的作用就是先根据newCapacity创建一个新的空数组,然后将旧数组的数据复制到新数组中。

private void grow() {            
    int oldCapacity = queue.length;            
    // 每次扩容增加原来数组的一半数量。
    // grow 50%
    int newCapacity = oldCapacity + (oldCapacity >> 1); 
    if (newCapacity < 0) // overflow
        newCapacity = Integer.MAX_VALUE;            
    // 使用Arrays.copyOf来复制一个新数组
    queue = Arrays.copyOf(queue, newCapacity);
}

5.4.3 向上堆化siftUp

新添加的元素先会加到堆底,然后一步步和上面的父亲节点比较,若小于父亲节点则和父亲节点互换位置,循环比较直至大于父亲节点才结束循环。通过循环,来查找元素key应该插入在堆二叉树哪个节点位置,并交互父节点的位置。

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;
    // 将key插入到k位置
    setIndex(key, k);
}

代码很好理解,就是循环的根据key节点与它的父节点来判断,如果key节点的执行时间早于父节点,则将两个节点交换,使执行时间靠前的节点排列在队列的前面。

假设新入队的节点的延迟时间(调用getDelay()方法获得)是 5 ,执行过程如下:

1、先将新的节点添加到数组的尾部,这时新节点的索引k为7

2、计算新父节点的索引:parent = (k - 1) >>> 1,parent = 3,那么queue[3]的时间间隔值为8,因为 5 < 8 ,将执行queue[7] = queue[3]

3、这时将k设置为3,继续循环,再次计算parent为1,queue[1]的时间间隔为3,因为 5 > 3 ,这时退出循环,最终k为3

可见,每次新增节点时,只是根据父节点来判断,而不会影响兄弟节点。

5.5 出队方法

DelayedWorkQueue 提供了以下几个出队方法

  • take(),等待获取队列头元素
  • poll() ,立即获取队列头元素
  • poll(long timeout, TimeUnit unit) ,超时等待获取队列头元素

5.5.1 take() 消费元素

Worker工作线程启动后就会循环消费工作队列中的元素,因为ScheduledThreadPoolExecutor的keepAliveTime=0,所以消费任务其只调用了DelayedWorkQueue.take。take()方法的基本流程如下:

  1. 首先获取可中断锁,判断堆顶元素是否是空,空的则阻塞等待available.await()。
  2. 堆顶元素不为空,则获取其延迟执行时间delay,delay <= 0说明到了执行时间,出队列finishPoll()。
  3. delay > 0还没到执行时间,判断leader线程是否为空,不为空则说明有其他take线程也在等待,当前take线程将无限期阻塞等待。
  4. leader线程为空,当前take线程设置为leader,并阻塞等待delay时长。
  5. 当前leader线程等待delay时长自动唤醒或者被其他take线程唤醒,则最终将leader设置为null。
  6. 再循环一次判断delay <= 0出队列。
  7. 跳出循环后判断leader为空并且堆顶元素不为空,则唤醒其他take线程,最后是否锁。
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
                    leader = thisThread;                            
                    try {                                
                        // 当任务的延时时间到了时,能够自动超时唤醒。
                        available.awaitNanos(delay);
                    } finally {                        
                                            // 唤醒后再次获得锁后把leader再置空     
                        if (leader == thisThread)
                            leader = null;
                    }
                }
            }
        }
    } finally {  
        // 唤醒等待任务的线程              
        if (leader == null && queue[0] != null)                    
            available.signal();
        ock.unlock();
    }
}

take基本流程图如下:

take线程阻塞等待:

可以看出这个生产者take线程会在两种情况下阻塞等待:

  • 堆顶元素为空。
  • 堆顶元素的delay > 0 。

take方法是什么时候调用的呢?

在ThreadPoolExecutor中,getTask方法,工作线程会循环地从workQueue中取任务。但定时任务却不同,因为如果一旦getTask方法取出了任务就开始执行了,而这时可能还没有到执行的时间,所以在take方法中,要保证只有在到指定的执行时间的时候任务才可以被取走。

leader线程

再来说一下leader的作用,这里的leader是为了减少不必要的定时等待。leader线程的设计,是Leader-Follower模式的变种,旨在于为了不必要的时间等待。当一个take线程变成leader线程(可以最先拿到队列中弹出任务去执行的线程)时,只需要等待下一次的延迟时间,而不是leader线程的其他take线程则需要等leader线程拿到队列中任务后,才唤醒其他take线程,其他的take线程唤醒后再去争抢锁,谁抢到锁了,谁就是新的leader线程。

举例来说,如果没有leader,那么在执行take时,所有的线程都要执行available.awaitNanos(delay),假设当前线程执行了该段代码,这时还没有signal,第二个线程也执行了该段代码,则第二个线程也要被阻塞。多个线程执行该段代码是没有作用的,因为只能有一个线程会从take中返回queue[0](因为有lock),其他线程这时再返回for循环执行时取的queue[0],已经不是之前的queue[0]了,然后又要继续阻塞。

所以,为了不让多个线程频繁的做无用的定时等待,这里增加了leader,如果leader不为空,则说明队列中第一个节点已经在等待出队,这时其它的线程会一直阻塞,减少了无用的阻塞(注意,在finally中调用了signal()来唤醒一个线程,而不是signalAll())。也就是说我们保证只会有一个线程在等待队列中的任务出队即可,其他的线程直接全部阻塞,当第一个线程成功获取到队列中的任务后再去唤醒其他的线程,来成为新的leader线程

5.5.2 finishPoll() 出队列

堆顶元素delay<=0,执行时间到,出队列就是一个向下堆化的过程siftDown。

// 移除队列头元素
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;
}

堆的删除方法主要分为三步:

  1. 先将队列中元素个数减一;
  2. 将原队列末尾元素设置成为队列头元素,再将队列末尾元素设置为null;
  3. 调用setDown(O,x)方法,保证按照元素的优先级排序。

5.5.3 向下堆化siftDown

由于堆顶元素出队列后,就破坏了堆的结构,需要组织整理下,将堆尾元素移到堆顶,然后向下堆化:

  1. 从堆顶开始,父亲节点与左右子节点中较小的孩子节点比较(左孩子不一定小于右孩子)。
  2. 父亲节点小于等于较小孩子节点,则结束循环,不需要交换位置。
  3. 若父亲节点大于较小孩子节点,则交换位置。
  4. 继续向下循环判断父亲节点和孩子节点的关系,直到父亲节点小于等于较小孩子节点才结束循环。
private void siftDown(int k, RunnableScheduledFuture<?> key) {     
    // 无符号右移,相当于size/2
    int half = size >>> 1;            
    // 通过循环,保证父节点的值不能大于子节点。
    while (k < half) {                
        // 左子节点, 相当于 (k * 2) + 1
        int child = (k << 1) + 1;                
        // 左子节点位置元素
        RunnableScheduledFuture<?> c = queue[child];                
        // 右子节点, 相当于 (k * 2) + 2
        int right = child + 1;                
        // 如果左子节点元素值大于右子节点元素值,那么右子节点才是较小值的子节点。
        // 就要将c与child值重新赋值
        if (right < size && c.compareTo(queue[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);
}

siftDown方法执行时包含两种情况,一种是没有子节点,一种是有子节点(根据half判断)。

例如:

没有子节点的情况:

假设初始的堆如下:

1、假设 k = 3 ,那么 k = half ,没有子节点,在执行siftDown方法时直接把索引为3的节点设置为数组的最后一个节点:

有子节点的情况:

假设 k = 0 ,那么执行以下步骤:

1、获取左子节点,child = 1 ,获取右子节点, right = 2 :

2、由于 right < size ,这时比较左子节点和右子节点时间间隔的大小,这里 3 < 7 ,所以 c = queue[child] ;

3、比较key的时间间隔是否小于c的时间间隔,这里不满足,继续执行,把索引为k的节点设置为c,然后将k设置为child;

4、因为 half = 3 ,k = 1 ,继续执行循环,这时的索引变为:

5、这时再经过如上判断后,将k的值为3,最终的结果如下:

6、最后,如果在finishPoll方法中调用的话,会把索引为0的节点的索引设置为-1,表示已经删除了该节点,并且size也减了1,最后的结果如下:

可见,siftdown方法在执行完并不是有序的,但可以发现,子节点的下次执行时间一定比父节点的下次执行时间要大,由于每次都会取左子节点和右子节点中下次执行时间最小的节点,所以还是可以保证在take和poll时出队是有序的。

5.5.4 poll()

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

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

5.5.5 poll(long timeout, TimeUnit unit)

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

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 < delay || leader != null)
                    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();
    }
}

5.5.6 remove() 删除指定元素

删除指定元素一般用于取消任务时,任务还在阻塞队列中,则需要将其删除。当删除的元素不是堆尾元素时,需要做堆化处理。

public boolean remove(Object x) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        int i = indexOf(x);
        if (i < 0)
            return false;
        //维护heapIndex
        setIndex(queue[i], -1);
        int s = --size;
        RunnableScheduledFuture<?> replacement = queue[s];
        queue[s] = null;
        if (s != i) {
            //删除的不是堆尾元素,则需要堆化处理
            //先向下堆化
            siftDown(i, replacement);
            if (queue[i] == replacement)
                //若向下堆化后,i位置的元素还是replacement,说明四无需向下堆化的,
                //则需要向上堆化
                siftUp(i, replacement);
        }
        return true;
    } finally {
        lock.unlock();
    }
}

假设初始的堆结构如下:

这时要删除8的节点,那么这时 k = 1,key为最后一个节点:

这时通过上文对siftDown方法的分析,siftDown方法执行后的结果如下:

这时会发现,最后一个节点的值比父节点还要小,所以这里要执行一次siftUp方法来保证子节点的下次执行时间要比父节点的大,所以最终结果如下:

六、总结

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

  • DelayedWorkQueue的数据结构是基于堆实现的;
  • DelayedWorkQueue采用数组实现堆,根节点出队,用最后叶子节点替换,然后下推至满足堆成立条件;最后叶子节点入队,然后向上推至满足堆成立条件;
  • DelayedWorkQueue添加元素满了之后会自动扩容原来容量的1/2,即永远不会阻塞,最大扩容可达Integer.MAX_VALUE,所以线程池中至多有corePoolSize个工作线程正在运行;
  • DelayedWorkQueue 消费元素take,在堆顶元素为空和delay >0 时,阻塞等待;
  • DelayedWorkQueue 是一个生产永远不会阻塞,消费可以阻塞的生产者消费者模式;
  • DelayedWorkQueue 有一个leader线程的变量,是Leader-Follower模式的变种。当一个take线程变成leader线程时,只需要等待下一次的延迟时间,而不是leader线程的其他take线程则需要等leader线程出队列了才唤醒其他take线程。

  相关文章:【线程池】Java的线程池
                  【线程池】Java线程池的核心参数
                  【线程池】Executors框架创建线程池        
                                  【线程池】ScheduledExecutorService接口和ScheduledThreadPoolExecutor定时任务线程池使用详解                 【线程池】线程池的拒绝策略(饱和策略) 

                【线程池】线程池的ctl属性详解
                【线程池】史上最全的ThreadPoolExecutor源码详解

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/706768.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

LangChain 基于 ChatGPT 构建本地知识库问答应用

一、使用本地知识库构建问答应用 上篇文章基于 LangChain 的Prompts 提示管理构建特定领域模型&#xff0c;如果看过应该可以感觉出来 ChatGPT 还是非常强大的&#xff0c;但是对于一些特有领域的内容让 GPT 回答的话还是有些吃力的&#xff0c;比如让 ChatGPT 介绍下什么是 L…

二重积分的解题技巧

计算方法 本节内容一般都应该先画图再思考后续内容较为直观 基本口诀是&#xff1a;后积先定限&#xff0c;限内画条线&#xff0c;先交写下限&#xff0c;后交写上限&#xff08;且下限必须小于上限&#xff09; 结合下图进行解释&#xff0c;后积先定限&#xff0c;对于X-型来…

Java基础-多线程JUC-多线程实现的三种形式

1. 第一种 继承Thread&#xff0c;重写run方法 public class demo1 {public static void main(String[] args) {/*** 多线程的第一种启动方式* 1. 定义一个类继承Thread* 2. 重写run方法* 3. 创建子类的对象&#xff0c;并启动线程*/MyThread myThread new MyThread();MyThrea…

受检异常和非受检异常

所谓的受检异常&#xff0c;表示在编译的时候强制检查的异常&#xff0c;这种异常需要显示的通过 try/catch 来捕捉&#xff0c;或者通过 throws 抛出去&#xff0c;否则从程序无法通过编译。 而非受检异常&#xff0c;表示在编译器可以不需要强制检查的异常&#xff0c;这种异…

Java基础---如何理解Java中的多态

目录 所谓多态 方法的重载与重写 重载和重写的区别 所谓多态 多态的概念比较简单&#xff0c;就是同一操作作用于不同的对象&#xff0c;可以有不同的解释&#xff0c;产生不同的执行结果如果按照这个概念来定义的话&#xff0c;那么多态应该是一种运行期的状态为了实现运行…

VScode 配置Orbslam debug调试环境(ubuntu18.04)

如果没有安装VScode的话&#xff0c;可以去点击官网下载 如果已经安装了VScode&#xff0c;可以直接把ORBSLAM的工程文件夹拖到VScode中&#xff0c;系统会提示是否添加C/C编译工具&#xff0c;直接安装即可&#xff0c;安装过程中也可以安装一下cmake 将ORBSLAM的工程文件夹拖…

obs编译

源码地址&#xff1a; GitHub - obsproject/obs-studio: OBS Studio - Free and open source software for live streaming and screen recording windows 编译&#xff1a; Build Instructions For Windows obsproject/obs-studio Wiki GitHub https://www.cnblogs.co…

app自动化测试(Android)--App 控件定位

客户端的页面通过 XML 来实现 UI 的布局&#xff0c;页面的 UI 布局作为一个树形结构&#xff0c;而树叶被定义为节点。这里的节点也就对应了要定位的元素&#xff0c;节点的上级节点&#xff0c;定义了元素的布局结构。在 XML 布局中可以使用 XPath 进行节点的定位。 App的布…

pytorch 迁移训练自己的数据集

1、pytorch 基础训练 上一节为基础 视频与AI&#xff0c;与进程交互(二) pytorch 极简训练自己的数据集并识别 接着上面一节&#xff0c;我们开始使用迁移学习&#xff0c;训练自己的数据集和保存网络&#xff0c;加载网络并识别。 2、 pytorch加载resnet18 RetNet网络的基础…

Vscode platformio Arduino开发STM32,点灯+串口调试

1.工具 USB-TTL(非常便宜&#xff0c;几块钱)STM32F103C8T6(几块钱) 2.引脚连线 USB-TTLSTM32TXPA10RXPA9VCC3.3VGNDGND 注意事项&#xff1a; 跳线帽位置&#xff1a;BOOT0接高电平(1)&#xff0c;BOOT1接低电平(0)每次上传程序前需要按一下复位键(之后&#xff0c;跳线帽…

互联网编程之基于 TCP 的单线程文件收发程序(CS架构)

目录 需求 服务端实现 客户端实现 测试 需求 可试着根据java编程课所学到的java socket编程技术&#xff0c;尝试编写一个基于 TCP 的单线程文件收发程序&#xff0c;需满足&#xff1a; 服务端程序预先定义好需要发送的文件并等待客户端的连接。 客户端连接成功后&…

ranger配置hive出錯:Unable to connect repository with given config for hive

ranger配置hive出錯&#xff1a;Unable to connect repository with given config for hive 我一開始我以為是我重啟了ranger-admin導致ranger有點問題&#xff0c;後面排查之後發現是我之前把hiveserver2關閉了&#xff0c;所以只需要重新開啟hiveserver2即可

开源 sysgrok — 用于分析、理解和优化系统的人工智能助手

作者&#xff1a;Sean Heelan 在这篇文章中&#xff0c;我将介绍 sysgrok&#xff0c;这是一个研究原型&#xff0c;我们正在研究大型语言模型 (LLM)&#xff08;例如 OpenAI 的 GPT 模型&#xff09;如何应用于性能优化、根本原因分析和系统工程领域的问题。 你可以在 GitHub …

一个真实的社会工程学攻击

社会工程学实例 不同于以往通过心理诱骗暗示或欺诈手段社会工程学举例&#xff0c;本次为大家介绍一种特殊的结合刑侦推理及利用技术手段实现的社会工程学实例&#xff0c;可以把它归类为特殊层面的信息收集手段——通过照片确定发拍照人所在的位置&#xff0c;这种社工手段严格…

ExtJS4 相关

2. 程序架构 2.1 目录结构 推荐下面这种目录结构(非强制&#xff0c;如果你足够懂和自信) - appname- app- namespace- Class1.js- Class2.js- ...- extjs- resources- css- images- ...- app.js- index.html appname 包含所有程序代码&#xff0c;是根目录 app 包含所有类&…

MySQL基础篇(day03,复习自用)

MySQL第三天 排序与分页内容练习 多表查询内容练习 排序与分页 内容 #第五章 排序与分页#1.排序 #如果没有使用排序操作&#xff0c;默认情况下查询返回的数据是按照添加数据的顺序显示的。 SELECT * FROM employees;#1.1基本使用 #使用 ORDER BY 对查询到的数据进行排序操作 …

JSON轻量级数据交换格式

文章目录 前言前后端数据交换格式比较JavaScript自定义对象 前言 JSON全名 &#xff1a;JavaScriptObjectNoation JSON在后端和前端中起着非常重要的作用。 在前端&#xff0c;JSON被用作一种数据格式&#xff0c;用于从服务器获取数据并将其展示在网页上&#xff1b; 前端开…

[SpringBoot]单点登录

关于单点登录 单点登录的基本实现思想&#xff1a; 当客户端提交登录请求时&#xff0c;服务器端在验证登录成功后&#xff0c;将生成此用户对应的JWT数据&#xff0c;并响应到客户端 客户端在后续的访问中&#xff0c;将自行携带JWT数据发起请求&#xff0c;通常&#xff0c…

LeetCode 2501 数组中最长的方波 Java

方法一&#xff0c;哈希表枚举 构造哈希集合&#xff0c;记录出现过的数字枚举遍历 import java.util.HashSet; import java.util.Set;class Solution {public int longestSquareStreak(int[] nums) {//构造哈希表集合&#xff0c;记录出现过的数字&#xff0c;转long型&…

kafka入门,Leader 和 Follower 故障处理细节(十四)

Leader 和 Follower 故障处理细节 LEO:每个腹部最后一个offset,leo其实就是最新的offset1 HW:所有副本中最小的LEO Follower故障处理细节 &#xff08;1&#xff09;Follower发生故障会被临时提出ISR (2) 这个期间leader和Follower积蓄接收数据 (3) 待该Follower恢复后&#…