SchduledThreadPoolExecutor线程池源码分析

一、架构和继承体系关系

1.1 架构图

在这里插入图片描述

1.2 核心类:ScheduledThreadPoolExecutor

在这里插入图片描述

1.3 内部类:DelayedWorkQueue

  • DelayedWorkQueue : 内部是一个 RunnableScheduledFuture接口的数组、存储了按照最小堆数据结构排列的延迟任务。
    在这里插入图片描述

1.4 ScheduledFutureTask 任务类

记录了任务执行时间、周期(period)、序号;实现了Comparable接口,重写了compareTo()方法,实现按照任务执行时间排序。
period = 0,代表延时任务
period > 0, 表示在任务开始时间+ period
period < 0, 表示在任务执行结束时间+ period
在这里插入图片描述

二、基本方法

2.1 延迟执行任务 schedule

//1.延迟执行
schedule(Runnable r, long delay, TimeUnit unit))
//有返回值
schedule(Callable r, long delay, TimeUnit unit))

2.2 scheduleAtFixedRate

scheduleAtFixedRate(Runnable command,long initialDelay,long period,
TimeUnit unit)

  • initialDelay 初始延迟多长时间执行。
  • period 初始执行时以后每隔多长时间周期执行一次,不论该任务执行多长时间,将下次执行任务扔到队列中。
    如图所示:
    在这里插入图片描述

2.3 scheduleWithFixedDelay

scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)

周期执行,与上面区别在于,该方法是在任务执行完毕后delay时间再执行,周期时间的开始时间不同
如图所示:
在这里插入图片描述
构造函数差异在这里,正负设置
在这里插入图片描述

三、核心方法源码解析

类名:DelayedWorkQueue

3.1 take() 获取队列第一个任务

public RunnableScheduledFuture<?> take() throws InterruptedException {
    //1.获取sheduled线程池的锁
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        for (;;) {
           //2.从队列中获取第一个任务
            RunnableScheduledFuture<?> first = queue[0];
            if (first == null)
            //3.如果任务为空,该线程阻塞等待
                available.await();
            else {
               //4.如果不为空,获取还有多长时间执行
                long delay = first.getDelay(NANOSECONDS);
                if (delay <= 0)
                //已到执行时间,早于或等于;返回任务,见下方法1
                    return finishPoll(first);
                //5.还没到任务执行时间,等待时将该引用置空,
                first = null; // don't retain ref while waiting
                //如果有leader线程在执行,当前线程阻塞等待
                if (leader != null)
                    available.await();
                else {
                   //将当前线程设置为leader线程
                    Thread thisThread = Thread.currentThread();
                    leader = thisThread;
                    try {
                        //线程阻塞等待delay时间
                        available.awaitNanos(delay);
                    } finally {
                       //阻塞等待唤醒,将leader线程置空,继续for循环
                        if (leader == thisThread)
                            leader = null;
                    }
                }
            }
        }
    } finally {
        if (leader == null && queue[0] != null)
            //当前线程已经获取到第一个任务,队列中还有其他任务待执行,唤醒一个其他阻塞线程
            available.signal();
        //释放锁    
        lock.unlock();
    }
}

方法1,取出第一个元素,并对堆重排序

//该方法在获取锁的情况下执行,不存在多线程问题
private RunnableScheduledFuture<?> finishPoll(RunnableScheduledFuture<?> f) {
    //队列元素个数-1
    int s = --size;
    //获取最后一个任务
    RunnableScheduledFuture<?> x = queue[s];
    //数组最后一个位置元素置空
    queue[s] = null;
    if (s != 0)
      //队列中不止一个任务,需要进行最小堆重排
        siftDown(0, x);
    setIndex(f, -1);
    return f;
}

/**
 * 如果是周期任务,给任务堆索引赋值
 * Sets f's heapIndex if it is a ScheduledFutureTask.
 */
private void setIndex(RunnableScheduledFuture<?> f, int idx) {
    if (f instanceof ScheduledFutureTask)
        ((ScheduledFutureTask)f).heapIndex = idx;
}

3.2 删除节点 下溯

方法2,最小堆自上而下重排序
堆前备概念知识:

  • 1.根节点始终在数组索引为0位置
  • 2.假设任意非根节点索引为i, 则其父节点索引为 (i-1)/2
  • 3.假设节点索引为i, 它的左子节点若存在,索引为 2i+1,右子节点若存在,索引为 2i+2
/**
 * Sifts element added at top down to its heap-ordered spot.
 * Call only when holding lock.
 * k 索引位置,一般为根节点0,key, 最后一个任务
 */
private void siftDown(int k, RunnableScheduledFuture<?> key) {
   // 等同于 size/2,获取数组中间位置索引,即左边第一个叶子节点
   // 根据上面公式3, half *2 +1 >= size,索引在大于等于size下标的元素为空,
   // 因此不存在子节点 
    int half = size >>> 1;
    //循环向下的截止条件是最后一个非叶子节点
    while (k < half) {
        //获取左子节点
        /**
        *    1
        *  2   3
        */
        int child = (k << 1) + 1;
        RunnableScheduledFuture<?> c = queue[child];
        //右子节点索引
        int right = child + 1;
        if (right < size && c.compareTo(queue[right]) > 0)
         //若右子节点存在,且小于左子节点,c赋值为右子节点,且child指向右子节点索引
            c = queue[child = right];
        //如果待排序节点都小于等于,原左右子节点,跳出循环    
        if (key.compareTo(c) <= 0)
            break;
        //1.将其左或右子节点,即较小的那个,向上移动;若相等,移动左子节点
        queue[k] = c;
        setIndex(c, k);
        //child此时是较小子节点位置,继续循环向下遍历
        k = child;
    }
    //k为将要插入的位置,将最后一个元素放在该位置,此时堆满足最小堆结构
    queue[k] = key;
    //同时给该节点设置堆索引
    setIndex(key, k);
}

3.3 offer(Runnable task) 添加任务

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)
           //扩容,每次1.5倍增加
            grow();
        //个数+1    
        size = i + 1;
        if (i == 0) {
        //若原来队列中任务为空,直接放入队列
            queue[0] = e;
            setIndex(e, 0);
        } else {
           //加入队列,更新堆顺序
            siftUp(i, e);
        }
        if (queue[0] == e) {
        //如果当前任务是最先执行的,将leader线程置空,唤醒一个线程处理
            leader = null;
            available.signal();
        }
    } finally {
        lock.unlock();
    }
    return true;
}

3.4 添加节点上溯siftUp()

自下向上堆排序

/**
 * Sifts element added at bottom up to its heap-ordered spot.
 * Call only when holding lock.
 * 插入是从队尾开始
 * k=size
 */
private void siftUp(int k, RunnableScheduledFuture<?> key) {
    while (k > 0) {
        //父节点索引位置
        int parent = (k - 1) >>> 1;
        RunnableScheduledFuture<?> e = queue[parent];
        //与父节点比较大小,若比父节点大,则保持位置不变,跳出循环
        if (key.compareTo(e) >= 0)
            break;
        //比父节点小,与父节点交互位置    
        queue[k] = e;
        setIndex(e, k);
        k = parent;
    }
    queue[k] = key;
    setIndex(key, k);
}

3.5 ScheduledFutureTask类compareTo()方法

通过重写该方法,实现最小堆排序
先比较任务执行时间,先执行的在前面,若相同则序号小的排在前,线程池维护了一个原子自增器,每个加入的任务,获取一个自增序号。

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

3.6 ScheduledFutureTask 的run()方法

public void run() {
    //判断是否为周期任务
    boolean periodic = isPeriodic();
    if (!canRunInCurrentRunState(periodic))
        cancel(false);
    else if (!periodic)
        //立即执行
        ScheduledFutureTask.super.run();
    else if (ScheduledFutureTask.super.runAndReset()) {
       //周期任务,执行完毕,设置下一次执行时间
        setNextRunTime();
        reExecutePeriodic(outerTask);
    }
}
/**
 * Sets the next time to run for a periodic task.
 * 设置下一次执行时间
 */
private void setNextRunTime() {
    long p = period;
    if (p > 0)
    // 大于0,基于上一次任务开始时间 + p
        time += p;
    else
    // p小于则当前时间+ -p 
        time = triggerTime(-p);
}
long triggerTime(long delay) {
    return now() +
        ((delay < (Long.MAX_VALUE >> 1)) ? delay : overflowFree(delay));
}

四、与Timer对比

Timer :

  • 1.timer是单线程的,如果定时任务多,某个任务执行时间长,会影响后面的任务执行;
  • 2.其中某个任务发送异常,未捕获,会导致线程退出,所有任务无法继续执行

ScheduledTheadPoolExecutor : 多线程的,线程退出,线程池会重新创建新的线程执行

五、总结

最小堆数据结构
Leader-follower模型:资源有效利用,既实现多线程并发处理任务,又保证不是每个任务都无效忙等待

六、应用场景

1.Nacos等注册中心应该心跳检测,定时向server发送心跳,检测服务是否可用
2.分布式锁续期、redisson框架WatchDog 每隔1/3 过期时间向redis续期等应用
3.服务启动定时上报服务器运行状态,cpu,内存等指标

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值