并发编程十 定时任务定时线程池

news2024/9/22 1:12:44

一 ScheduledThreadPoolExecutor

定时线程池类的类结构图
在这里插入图片描述

在这里插入图片描述
它接收SchduledFutureTask类型的任务,是线程池调度任务的最小单位,有三种提交任务的方式:

  1. schedule
  2. scheduledAtFixedRate
  3. scheduledWithFixedDelay

它采用DelayQueue存储等待的任务

  1. DelayQueue内部封装了一个PriorityQueue,它会根据time的先后时间排序,若time相同则根据sequenceNumber排序;
  2. DelayQueue也是一个无界队列;

1.1 ScheduledThreadPoolExecutor

延迟执行,只能执行一次

import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @author :sgw
 * @date :Created in 2022/7/17
 * @version: V1.0
 * @slogan: 
 * @description: 场景:项目完全启动后需要执行一些任务(防止项目没有完全启动就去执行的话,可能有些过滤器、拦截器还没有加载完,spring里的bean等还没有初始化完而报错....)
 * 
 **/
@Slf4j
public class ScheduleThreadPoolRunner {

    public static void main(String[] args) {
        //参数:线程数
        ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(1);

        scheduledThreadPoolExecutor.schedule(() -> {
            System.out.println("我要延迟5s执行");
        }, 5000, TimeUnit.MILLISECONDS);
    }
}

当定时任务执行完后,要继续执行下一个业务逻辑,可以这样做:

 ScheduledFuture<Integer> future = scheduledThreadPoolExecutor.schedule(() -> {
            System.out.println("我要延迟3s执行");
            //这里可以返回任意类型数据
            return 1;
 }, 3000, TimeUnit.MILLISECONDS);

//这里主线程可以接着干其他活
//XXXXXXXXXXXXXXXXXXXXXX 

  //提交任务的线程-接着干活
   try {
   //得到的是1后,说明定时任务执行完了,接着做下一个业务
       System.out.println(future.get());
   } catch (InterruptedException e) {
       e.printStackTrace();
   } catch (ExecutionException e) {
       e.printStackTrace();
   }

周期定时任务

上边的代码,只能执行一次定时任务,那么想要周期性的执行定时任务

//参数:线程数(这里设置只有一个线程)
ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(1);

//发心跳,service1->service2,每次过2s,发送一个心跳,证明s2可用
scheduledThreadPoolExecutor.scheduleAtFixedRate(() -> {
     log.info("send heart beat");
     //线程启动1秒后执行上边的任务逻辑,并且每2秒执行一次上边的任务
 }, 1000, 2000, TimeUnit.MILLISECONDS);

在这里插入图片描述
上边代码可能有问题,假如任务的执行时间比较长(10秒),但是间隔时间是2秒执行一次(只有一个线程)

 //参数:线程数
ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(1);

scheduledThreadPoolExecutor.scheduleAtFixedRate(() -> {
	log.info("send heart beat");
	long starttime = System.currentTimeMillis(), nowtime = starttime;
	while ((nowtime - starttime) < 5000) {
		 nowtime = System.currentTimeMillis();
		 try {
		     Thread.sleep(100);
		 } catch (InterruptedException e) {
		     e.printStackTrace();
		 }
	 }
	log.info("task over....");
	//如果这里抛一个异常的话,就卡在这里了,不会向下执行了;所以有一次的话一定要捕获
   //throw new RuntimeException("unexpected error , stop working");
	}, 1000, 2000, TimeUnit.MILLISECONDS);

在这里插入图片描述
虽然周期时间到了,但是需要等上一个任务执行完后再执行下一个任务:

 //参数:线程数
ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(1);

scheduledThreadPoolExecutor.scheduleWithFixedDelay(() -> {
  log.info("send heart beat");
   long starttime = System.currentTimeMillis(), nowtime = starttime;
   while ((nowtime - starttime) < 5000) {
       nowtime = System.currentTimeMillis();
       try {
           Thread.sleep(100);
       } catch (InterruptedException e) {
           e.printStackTrace();
       }
   }
   log.info("task over....");
   //如果这里抛一个异常的话,
   //throw new RuntimeException("unexpected error , stop working");
}, 1000, 2000, TimeUnit.MILLISECONDS);

在这里插入图片描述
多个线程数的话,要想起作用,得需要开启多个scheduledThreadPoolExecutor 任务,如果只有一个scheduledThreadPoolExecutor任务的话,这里参数写多个线程的话没有用

 //参数:线程数如果是2
ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(2);

任务周期执行的其他类——定时类:Timer

//定时类
 Timer timer = new Timer();
 timer.scheduleAtFixedRate(new TimerTask() {
     @Override
     public void run() {
         log.info("send heart beat");
         //由于是单线程的,这里抛异常后,线程就没了,所以下边的timer任务就无法执行了
         throw new RuntimeException("unexpected error , stop working");
     }
 }, 1000, 2000);

 try {
     Thread.sleep(5000);
 } catch (InterruptedException e) {
     e.printStackTrace();
 }
//由于上边抛异常了,所以这里不能继续执行
 timer.scheduleAtFixedRate(new TimerTask() {
     @Override
     public void run() {
         log.info("send heart beat");
         throw new RuntimeException("unexpected error , stop working");
     }
 }, 1000, 2000);

在这里插入图片描述

阿里规范不建议使用,因为有异常后容易挂掉整个线程,推荐使用上边的ScheduledThreadPoolExecutor

面试:Time是单线程的吗?抛异常后会发生什么现象?
答:是单线程的,跑异常后会把整个线程停掉,导致后续其他任务不能继续执行;
在这里插入图片描述

场景一:分布式锁-redis
通过setNx(“”,锁过期时间),如果拿到锁,就去执行业务逻辑;没拿到的话sleep,循环一直尝试拿锁;

//获取分布式锁
if(setNx("",锁过期时间)){
执行业务逻辑......
释放锁
}else{
sleep循环获取锁
}

分布式锁可以使用zookeeper、redis、DB等来实现,zookeeper可靠性比redis好,但是高并发、高吞吐量场景下性能上redis要好;

redis可靠性不好的原因:锁已经过期了,但是业务逻辑还没有执行完——锁的过期时间我们不确定;
解决方式:定时任务,每2秒续约(续命)一次,有框架已经实现了这个续命(redisson)

伪代码如下:

//获取分布式锁
if(setNx("",锁过期时间)){
	定时线程池:2秒续命一次,看看redis锁还在不在,延长时间(相当于redission框架里的看门狗概念)
	执行业务逻辑......
	释放锁
}else{
	sleep循环获取锁
}

1.2 SchduledFutureTask

SchduledFutureTask接收的参数(成员变量):

private long time:任务开始的时间
private final long sequenceNumber;:任务的序号
private final long period:任务执行的时间间隔

工作线程的执行过程:

  • 工作线程会从DelayQueue取已经到期的任务去执行;
  • 执行结束后重新设置任务的到期时间,再次放回DelayQueue

ScheduledThreadPoolExecutor会把待执行的任务放到工作队列DelayQueue中,DelayQueue封装了一个PriorityQueue,PriorityQueue会对队列中的ScheduledFutureTask进行排序,具体的排序算法实现如下:

public int compareTo(Delayed other) {
    if (other == this) // compare zero if same object
        return 0;
    if (other instanceof ScheduledFutureTask) {
        ScheduledFutureTask<?> x = (ScheduledFutureTask<?>)other;
        long diff = time - x.time;
        if (diff < 0)
            return -1;
        else if (diff > 0)
            return 1;
        else if (sequenceNumber < x.sequenceNumber)
            return -1;
        else
            return 1;
    }
    long diff = getDelay(NANOSECONDS) - other.getDelay(NANOSECONDS);
    return (diff < 0) ? -1 : (diff > 0) ? 1 : 0;
}
  1. 首先按照time排序,time小的排在前面,time大的排在后面;
  2. 如果time相同,按照sequenceNumber排序,sequenceNumber小的排在前面,sequenceNumber大的排在后面,换句话说,如果两个task的执行时间相同,优先执行先提交的task。

SchduledFutureTask之run方法实现
run方法是调度task的核心,task的执行实际上是run方法的执行。

public void run() {
    boolean periodic = isPeriodic();
//如果当前线程池已经不支持执行任务,则取消
    if (!canRunInCurrentRunState(periodic))
        cancel(false);
//如果不需要周期性执行,则直接执行run方法然后结束
    else if (!periodic)
        ScheduledFutureTask.super.run();
//如果需要周期执行,则在执行完任务以后,设置下一次执行时间
    else if (ScheduledFutureTask.super.runAndReset()) {
        // 计算下次执行该任务的时间
 setNextRunTime();
 //重复执行任务
        reExecutePeriodic(outerTask);
    }
}
  1. 如果当前线程池运行状态不可以执行任务,取消该任务,然后直接返回,否则执行步骤2;
  2. 如果不是周期性任务,调用FutureTask中的run方法执行,会设置执行结果,然后直接返回,否则执行步骤3;
  3. 如果是周期性任务,调用FutureTask中的runAndReset方法执行,不会设置执行结果,然后直接返回,否则执行步骤4和步骤5;
  4. 计算下次执行该任务的具体时间;
  5. 重复执行任务。

reExecutePeriodic方法

void reExecutePeriodic(RunnableScheduledFuture<?> task) {
    if (canRunInCurrentRunState(true)) {
        super.getQueue().add(task);
        if (!canRunInCurrentRunState(true) && remove(task))
            task.cancel(false);
        else
            ensurePrestart();
    }
}

该方法和delayedExecute方法类似,不同的是:

  1. 由于调用reExecutePeriodic方法时已经执行过一次周期性任务了,所以不会reject当前任务;
  2. 传入的任务一定是周期性任务。

线程池任务的提交
首先是schedule方法,该方法是指任务在指定延迟时间到达后触发,只会执行一次。

public ScheduledFuture<?> schedule(Runnable command,
                                   long delay,
                                   TimeUnit unit) {
//参数校验
if (command == null || unit == null)
        throw new NullPointerException();
//这里是一个嵌套结构,首先把用户提交的任务包装成ScheduledFutureTask
//然后在调用decorateTask进行包装,该方法是留给用户去扩展的,默认是个空方法
    RunnableScheduledFuture<?> t = decorateTask(command,
        new ScheduledFutureTask<Void>(command, null,
                                      triggerTime(delay, unit)));
   //包装好任务以后,就进行提交了
delayedExecute(t);
    return t;
}

任务提交方法:

private void delayedExecute(RunnableScheduledFuture<?> task) {
    //如果线程池已经关闭,则使用拒绝策略把提交任务拒绝掉
if (isShutdown())
        reject(task);
    else {
//与ThreadPoolExecutor不同,这里直接把任务加入延迟队列
        super.getQueue().add(task);//使用用的DelayedWorkQueue
//如果当前状态无法执行任务,则取消
        if (isShutdown() &&
            !canRunInCurrentRunState(task.isPeriodic()) &&
            remove(task))
            task.cancel(false);
        else
//这里是增加一个worker线程,避免提交的任务没有worker去执行
//原因就是该类没有像ThreadPoolExecutor一样,woker满了才放入队列
          ensurePrestart();
    }
}

1.3 DelayedWorkQueue

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

堆结构如下图:
在这里插入图片描述
可见,DelayedWorkQueue是一个基于最小堆结构的队列。堆结构可以使用数组表示,可以转换成如下的数组:
在这里插入图片描述
在这种结构中,可以发现有如下特性:
假设,索引值从0开始,子节点的索引值为k,父节点的索引值为p,则:

  1. 一个节点的左子节点的索引为:k = p * 2 + 1;
  2. 一个节点的右子节点的索引为:k = (p + 1) * 2;
  3. 一个节点的父节点的索引为:p = (k - 1) / 2。

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

DelayedWorkQueue属性

// 队列初始容量
private static final int INITIAL_CAPACITY = 16;
// 根据初始容量创建RunnableScheduledFuture类型的数组
private RunnableScheduledFuture<?>[] queue =
    new RunnableScheduledFuture<?>[INITIAL_CAPACITY];
private final ReentrantLock lock = new ReentrantLock();
private int size = 0;
// leader线程
private Thread leader = null;
// 当较新的任务在队列的头部可用时,或者新线程可能需要成为leader,则通过该条件发出信号
private final Condition available = lock.newCondition();

注意这里的leader,它是Leader-Follower模式的变体,用于减少不必要的定时等待。什么意思呢?对于多线程的网络模型来说:

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

offer方法

public boolean offer(Runnable x) {
//参数校验
    if (x == null)
        throw new NullPointerException();
    RunnableScheduledFuture<?> e = (RunnableScheduledFuture<?>)x;
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
//查看当前元素数量,如果大于队列长度则进行扩容
        int i = size;
        if (i >= queue.length)
            grow();
//元素数量加1
        size = i + 1;
//如果当前队列还没有元素,则直接加入头部
        if (i == 0) {
            queue[0] = e;
//记录索引
            setIndex(e, 0);
        } else {
 //把任务加入堆中,并调整堆结构,这里就会根据任务的触发时间排列
             //把需要最早执行的任务放在前面
            siftUp(i, e);
        }
//如果新加入的元素就是队列头,这里有两种情况
        //1.这是用户提交的第一个任务
        //2.新任务进行堆调整以后,排在队列头
        if (queue[0] == e) {
// leader设置为null为了使在take方法中的线程在通过available.signal();后会执行available.awaitNanos(delay);
            leader = null;
//加入元素以后,唤醒worker线程
            available.signal();
        }
    } finally {
        lock.unlock();
    }
    return true;
}

任务排序sift方法

private void siftUp(int k, RunnableScheduledFuture<?> key) {
// 找到父节点的索引
while (k > 0) {
  // 获取父节点
        int parent = (k - 1) >>> 1;
        RunnableScheduledFuture<?> e = queue[parent];
  // 如果key节点的执行时间大于父节点的执行时间,不需要再排序了
        if (key.compareTo(e) >= 0)
            break;
 // 如果key.compareTo(e) < 0,说明key节点的执行时间小于父节点的执行时间,需要把父节点移到后面
        queue[k] = e;
        setIndex(e, k);
// 设置索引为k
        k = parent;
    }
// key设置为排序后的位置中
    queue[k] = key;
    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:
    在这里插入图片描述
    可见,每次新增节点时,只是根据父节点来判断,而不会影响兄弟节点。

take方法

public RunnableScheduledFuture<?> take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        for (;;) {
            RunnableScheduledFuture<?> first = queue[0];
            if (first == null)
                available.await();
            else {
// 计算当前时间到执行时间的时间间隔
                long delay = first.getDelay(NANOSECONDS);
                if (delay <= 0)
                    return finishPoll(first);
                first = null; // don't retain ref while waiting
                // leader不为空,阻塞线程
if (leader != null)
                    available.await();
                else {
                    // leader为空,则把leader设置为当前线程,
Thread thisThread = Thread.currentThread();
                    leader = thisThread;
                    try {
                       // 阻塞到执行时间 
available.awaitNanos(delay);
                    } finally {
// 设置leader = null,让其他线程执行available.awaitNanos(delay);
                        if (leader == thisThread)
                            leader = null;
                    }
                }
            }
        }
    } finally {
// 如果leader不为空,则说明leader的线程正在执行available.awaitNanos(delay);
        // 如果queue[0] == null,说明队列为空
        if (leader == null && queue[0] != null)
            available.signal();
        lock.unlock();
    }
}

take方法是什么时候调用的呢?在ThreadPoolExecutor中,介绍了getTask方法,工作线程会循环地从workQueue中取任务。但定时任务却不同,因为如果一旦getTask方法取出了任务就开始执行了,而这时可能还没有到执行的时间,所以在take方法中,要保证只有在到指定的执行时间的时候任务才可以被取走。
再来说一下leader的作用,这里的leader是为了减少不必要的定时等待,当一个线程成为leader时,它只等待下一个节点的时间间隔,但其它线程无限期等待。 leader线程必须在从take()或poll()返回之前signal其它线程,除非其他线程成为了leader。
举例来说,如果没有leader,那么在执行take时,都要执行available.awaitNanos(delay),假设当前线程执行了该段代码,这时还没有signal,第二个线程也执行了该段代码,则第二个线程也要被阻塞。多个这时执行该段代码是没有作用的,因为只能有一个线程会从take中返回queue[0](因为有lock),其他线程这时再返回for循环执行时取的queue[0],已经不是之前的queue[0]了,然后又要继续阻塞。
所以,为了不让多个线程频繁的做无用的定时等待,这里增加了leader,如果leader不为空,则说明队列中第一个节点已经在等待出队,这时其它的线程会一直阻塞,减少了无用的阻塞(注意,在finally中调用了signal()来唤醒一个线程,而不是signalAll())。

poll 方法
下面看下poll方法,与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) {
                if (nanos <= 0)
                    return null;
                else
                    nanos = available.awaitNanos(nanos);
            } else {
                long delay = first.getDelay(NANOSECONDS);
// 如果delay <= 0,说明已经到了任务执行的时间,返回。
                if (delay <= 0)
                    return finishPoll(first);
// 如果nanos <= 0,说明已经超时,返回null
                if (nanos <= 0)
                    return null;
                first = null; // don't retain ref while waiting
  // nanos < delay 说明需要等待的时间小于任务要执行的延迟时间
                // leader != null 说明有其它线程正在对任务进行阻塞
                // 这时阻塞当前线程nanos纳秒
                if (nanos < delay || leader != null)
                    nanos = available.awaitNanos(nanos);
                else {
                    Thread thisThread = Thread.currentThread();
                    leader = thisThread;
                    try {
// 这里的timeLeft表示delay减去实际的等待时间
                        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();
    }
}

finishPoll方法
当调用了take或者poll方法能够获取到任务时,会调用该方法进行返回:

private RunnableScheduledFuture<?> finishPoll(RunnableScheduledFuture<?> f) {
// 数组长度-1
    int s = --size;
// 取出最后一个节点
    RunnableScheduledFuture<?> x = queue[s];
    queue[s] = null;
// 长度不为0,则从第一个元素开始排序,目的是要把最后一个节点放到合适的位置上
    if (s != 0)
        siftDown(0, x);
    setIndex(f, -1);
    return f;
}

siftDown方法
siftDown方法使堆从k开始向下调整:

private void siftDown(int k, RunnableScheduledFuture<?> key) {
// 根据二叉树的特性,数组长度除以2,表示取有子节点的索引
    int half = size >>> 1;
// 判断索引为k的节点是否有子节点
    while (k < half) {
// 左子节点的索引
        int child = (k << 1) + 1;
        RunnableScheduledFuture<?> c = queue[child];
// 右子节点的索引
        int right = child + 1;
// 如果有右子节点并且左子节点的时间间隔大于右子节点,取时间间隔最小的节点
        if (right < size && c.compareTo(queue[right]) > 0)
            c = queue[child = right];
// 如果key的时间间隔小于等于c的时间间隔,跳出循环
        if (key.compareTo(c) <= 0)
            break;
// 设置要移除索引的节点为其子节点
        queue[k] = c;
        setIndex(c, k);
        k = child;
    }
// 将key放入索引为k的位置
    queue[k] = key;
    setIndex(key, k);
}

siftDown方法执行时包含两种情况,一种是没有子节点,一种是有子节点(根据half判断)。例如:
没有子节点的情况:
假设初始的堆如下:
在这里插入图片描述
假设 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时出队是有序的。

remove方法

public boolean remove(Object x) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        int i = indexOf(x);
        if (i < 0)
            return false;

        setIndex(queue[i], -1);
        int s = --size;
        RunnableScheduledFuture<?> replacement = queue[s];
        queue[s] = null;
        if (s != i) {
  // 从i开始向下调整
            siftDown(i, replacement);
 // 如果queue[i] == replacement,说明i是叶子节点
            // 如果是这种情况,不能保证子节点的下次执行时间比父节点的大
            // 这时需要进行一次向上调整
            if (queue[i] == replacement)
                siftUp(i, replacement);
        }
        return true;
    } finally {
        lock.unlock();
    }
}

假设初始的堆结构如下:
在这里插入图片描述
这时要删除8的节点,那么这时 k = 1,key为最后一个节点:
在这里插入图片描述
这时通过上文对siftDown方法的分析,siftDown方法执行后的结果如下:
在这里插入图片描述
这时会发现,最后一个节点的值比父节点还要小,所以这里要执行一次siftUp方法来保证子节点的下次执行时间要比父节点的大,所以最终结果如下:
在这里插入图片描述
总结
主要总结为以下几个方面:

  • 与Timer执行定时任务的比较,相比Timer,ScheduedThreadPoolExecutor有什么优点;
  • ScheduledThreadPoolExecutor继承自ThreadPoolExecutor,所以它也是一个线程池,也有coorPoolSize和workQueue,ScheduledThreadPoolExecutor特殊的地方在于,自己实现了优先工作队列DelayedWorkQueue;
  • ScheduedThreadPoolExecutor实现了ScheduledExecutorService,所以就有了任务调度的方法,如schedule,scheduleAtFixedRate和scheduleWithFixedDelay,同时注意他们之间的区别;
  • 内部类ScheduledFutureTask继承自FutureTask,实现了任务的异步执行并且可以获取返回结果。同时也实现了Delayed接口,可以通过getDelay方法获取将要执行的时间间隔;
  • 周期任务的执行其实是调用了FutureTask类中的runAndReset方法,每次执行完不设置结果和状态。
  • 详细分析了DelayedWorkQueue的数据结构,它是一个基于最小堆结构的优先队列,并且每次出队时能够保证取出的任务是当前队列中下次执行时间最小的任务。同时注意一下优先队列中堆的顺序,堆中的顺序并不是绝对的,但要保证子节点的值要比父节点的值要大,这样就不会影响出队的顺序。
    总体来说,ScheduedThreadPoolExecutor的重点是要理解下次执行时间的计算,以及优先队列的出队、入队和删除的过程,这两个是理解ScheduedThreadPoolExecutor的关键。

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

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

相关文章

带你玩转序列模型之Bleu得分注意力模型语音识别

目录 一.Bleu得分 二.注意力模型直观理解 三.注意力模型 四.语音识别 五.触发字检测 一.Bleu得分 先跳过&#xff0c;等回头用得到了再来补。 二.注意力模型直观理解 在本周大部分时间中&#xff0c;你都在使用这个编码解码的构架&#xff08;a Encoder-Decoder archit…

MATLAB算法实战应用案例精讲-【图像处理】目标检测

前言 目标检测,也叫目标提取,是一种基于目标几何和统计特征的图像分割。它将目标的分割和识别合二为一,其准确性和实时性是整个系统的一项重要能力。尤其是在复杂场景中,需要对多个目标进行实时处理时,目标自动提取和识别就显得特别重要。 随着计算机技术的发展和计算机视…

AutoSAR基础:Port与Dio

AutoSAR基础:Port与Dio配置一.配置port1.Configurator1-1.进入Basic Editor->Port1-2.配置P00.0脚1-2-1.Port口1-2-2.Pin脚配置1-3.配置P00.11-3-1.Pin脚配置2.config导出生成代码3.代码部分3-1.config生成代码3-2.Write与Read3-2-1.Read函数3-2-2.Write函数二.配置Dio1.Con…

[附源码]计算机毕业设计基于SpringBoot的校园报修平台

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

qt使用http get和post

qt使用http get和post 本文目录qt使用http get和post准备get请求widge.hwidget.cpppost请求widge.hwidget.cpppost请求的数据格式参数写在url后面使用application/x-www-form-urlencoded使用application/json使用multipart/form-data传送文件准备 使用到网络编程&#xff0c;需…

Nginx配置实例-反向代理

1、实现效果 打开浏览器&#xff0c;在浏览器地址栏输入地址www.123.com&#xff0c;访问tomcat主页面。 2、准备工作 &#xff08;1&#xff09;在 liunx系统安装tomcat&#xff0c;使用默认端口8080 上传安装文件&#xff0c;并解压缩到/opt/tomcat。 进入tomcat的bin目录中…

[附源码]Python计算机毕业设计Django人员信息管理

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;…

ArrayList为什么线程不安全以及三种解决办法【详细】

目录不安全原因解决办法VectorCollectionsCopyOnWriteArrayList三种解决方式总结不安全原因 我们可以看一下ArrayList源码&#xff0c;找到add方法&#xff0c; public boolean add(E e) {ensureCapacityInternal(size 1); // Increments modCount!!elementData[size] e;r…

【java】Lambda表达式

文章目录体验Lambda表达式Lambda表达式的标准格式Lambda表达式的练习抽象方法无参无返回值抽象方法带参无返回值抽象方法带参带返回值Lambda表达式的省略模式Lambda表达式的注意事项Lambda表达式和匿名内部类的区别体验Lambda表达式 package heima.Lambda;import heima.多线程.…

HTML5期末大作业:旅游网页设计与实现——旅游风景区网站HTML+CSS+JavaScript 景点静态网页设计 学生DW静态网页设计

&#x1f468;‍&#x1f393;静态网站的编写主要是用 HTML DⅣV CSSJS等来完成页面的排版设计&#x1f469;‍&#x1f393;&#xff0c;一般的网页作业需要融入以下知识点&#xff1a;div布局、浮动定位、高级css、表格、表单及验证、js轮播图、音频视频Fash的应用、uli、下拉…

JMeter入门教程(9) --参数化

文章目录1.任务背景2.任务目标3.任务实操3.1 CSV数据文件1.任务背景 参数化是测试过程中很常用的一种技巧&#xff0c;可以将脚本中的某些输入用参数来代替&#xff0c;比如登陆时传递参数&#xff0c;在脚本运行时指定参数的取值范围和规则 2.任务目标 掌握基于JMeter性能测…

【浅学Java】SpringBoot创建和使用

SpringBoot创建和使用1. SpringBoot是什么2. SpringBoot的优点3. SpringBoot的创建3.1 使用idea创建3.2 测试项目_输出hello3.3 网页版创建SpringBoot4. 约定大于配置1. SpringBoot是什么 Spring的诞生是为了简化Java开发而创建的&#xff0c;而SpringBoot的诞生就是为了简化S…

一文带你理解【自然语言处理(NLP)】的基本概念及应用

觉得有帮助请点赞关注收藏~~~ 1.1 自然语言处理 1.1.1 自然语言处理主要研究对象 自然语言处理&#xff08;Natural Language Processing&#xff1a;NLP&#xff09;是以人类社会的语言信息&#xff08;比如语音和文本&#xff09;为主要研究对象&#xff0c;利用计算机技术来…

C++:深拷贝和浅拷贝——拷贝构造、赋值构造必须自定义

https://www.bilibili.com/video/BV1qT4y1X7cQ/?spm_id_from333.337.search-card.all.click&vd_sourced33b44674c517c8b7928e8d3ac316b37 1、赋值运算符重载 浅拷贝的错误代码&#xff1a; class Distance { public:int* dis NULL;Distance(int a){dis new int(a);}~…

轻量级xshell+manager远程监控jvisualvm

一、服务器端&#xff08;Linux&#xff0c;最小安装模式&#xff0c;没有图形界面&#xff09; 1.安装xauth 如果在Xshell中配置了X11转发后&#xff0c;出现如下提示&#xff1a; WARNING! The remote SSH server rejected X11 forwarding request. 则需要查看/etc/ssh/s…

esxi 6.7下安装openwrt(iStoreOS)网卡直通

esxi 6.7下安装openwrt&#xff08;iStoreOS&#xff09;网卡直通 esxi上创建一个iStoreOS系统的虚拟机&#xff0c;当主路由使用&#xff0c;网卡直通方式 1、工具 硬件&#xff1a; 工控机&#xff1a;装有esxi6.7系统&#xff08;192.168.100.2&#xff09;&#xff0c;配…

图像仿射变换与双线性插值

图像变换 下面的所有变换假设都是针对一幅图像&#xff0c;即一个三维数组&#xff08;HWC&#xff09;&#xff0c;这里为简单起见&#xff0c;假设图像都是单通道&#xff08;C1&#xff09;的。 (x,y)(x,y)(x,y): 原图像中某一点 A 的位置(x′,y′)(x′,y′)(x′,y′): 变换…

记一次系统重装后电脑的优化设置

总目录 文章目录总目录前言一、Windows设置1.系统2.应用3.隐私4.更新与安全二、系统使用偏好设置1.设置远程协助2.文件资源管理器偏好设置3.用户账户控制设置4.修改桌面文件的路径5.根据需求删除系统自带的无用的软件总结前言 由于之前总是电脑用着用着C盘不是满了&#xff0c…

Spring框架(九):Spring注解开发Annotation

Spring注解开发引子如何用注解替代xml基础配置Bean可以加一些注解来实现原有的xml文件的功能Component注解及其衍生注解依赖注入AutowireSpring非自定义的注解开发Spring其他注解注解的原理解析-xml方式注解的原理解析-注解方式注解整合MyBatis框架注解整合第三方框架引子 痛定…

一篇文章让你搞懂Java顺序表

目录 一、 线性表的基本介绍 二、顺序表 1、顺序表的概念 2. 创建顺序表类&#xff08;ArrayList&#xff09; 2. 增加元素 3. 删除元素 4. 修改某个元素 5. 查找元素 Main类 在数据结构体系中我们将整个数据结构分为两类&#xff0c;一类是线性结构&#xff1b; 线性…