Java Executor源码解析(6)—ScheduledThreadPoolExecutor调度线程池源码解析【一万字】

此前我们学习了ThreadPoolExecutor的源码,现在我们来学习ScheduledThreadPoolExecutor的源码。

系列文章:

  1. Java Executor源码解析(1)—Executor执行框架的概述
  2. Java Executor源码解析(2)—ThreadPoolExecutor线程池的介绍和基本属性【一万字】
  3. Java Executor源码解析(3)—ThreadPoolExecutor线程池execute核心方法源码【一万字】
  4. Java Executor源码解析(4)—ThreadPoolExecutor线程池submit方法以及FutureTask源码【一万字】
  5. Java Executor源码解析(5)—ThreadPoolExecutor线程池其他方法的源码
  6. Java Executor源码解析(6)—ScheduledThreadPoolExecutor调度线程池源码解析【一万字】
  7. Java Executor源码解析(7)—Executors线程池工厂以及四大内置线程池

1 ScheduledThreadPoolExecutor的概述

public interface ScheduledExecutorService
extends ExecutorService

ScheduledExecutorService是ExecutorService的子接口,对ExecutorService做了扩展,可安排在给定的延迟后运行或定期执行的命令。一般使用其实现类ScheduledThreadPoolExecutor,Executors 类也为ScheduledExecutorService 实现提供了便捷的工厂方法newSingleThreadScheduledExecutor()。

public class ScheduledThreadPoolExecutor
extends ThreadPoolExecutor
implements ScheduledExecutorService

ScheduledExecutorService的核心实现类之一,它可另行安排在给定的延迟后运行命令,或者定期执行命令。其性能由于Timer,Timer对应的是单个后台线程,而ScheduledThreadPoolExecutor则可以启动多个后台线程,同时具有其他额外功能。Timer中一个任务出现异常之后会影响其他任务的执行,但是ScheduledThreadPoolExecutor不会。Timer中一个任务耗时较常会影响其他任务的执行,ScheduledThreadPoolExecutor不会。

继承了ThreadPoolExecutor,因此核心原理都是相同的,但是做了特性化处理,固定使用自己内部实现的DelayedWorkQueue作为无界阻塞延迟队列,类似于DelayQueue,采用小顶堆实现以及通过延迟时间比较大小,每次出队列的都是剩余延迟时间为0的任务,周期任务的原理简单说就是从队列中取任务出来执行一次之后如果发现是周期任务那么继续丢到队列中。由于是无界队列,因此仅使用corePoolSize 线程,maximumPoolSize线程的设置和调整都是没用的。线程任务也是使用自己内部实现的ScheduledFutureTask作为传递进来任务的延迟特性包装。

在这里插入图片描述

在学习ScheduledThreadPoolExecutor之前,应该先学习ThreadPoolExecutor基本线程池、DelayQueue延时任务队列、PriorityBlockingQueue优先任务队列、小顶堆数据结构等相关知识。JUC—两万字的PriorityBlockingQueue源码深度解析JUC—DelayQueue源码深度解析

2 ScheduledThreadPoolExecutor的重要属性

继承了ThreadPoolExecutor的非私有属性,同时具有自己的一些新属性,这些属性都比较简单,主要是用于控制定时任务和周期任务的执行条件的,详见注释。有趣的是超常的属性名字!

//继承了ThreadPoolExecutor的非私有属性,同时具有自己的一些新属性

/**
 * 关闭线程池之后(SHUTDOWN状态)是否继续执行周期任务,true表示是,false表示否,默认false
 */
private volatile boolean continueExistingPeriodicTasksAfterShutdown;

/**
 * 关闭线程池之后(SHUTDOWN状态)是否继续执行延迟一次性任务,true表示是,false表示否,默认true
 */
private volatile boolean executeExistingDelayedTasksAfterShutdown = true;

/**
 * 执行ScheduledFutureTask.cancel取消任务的时候是否从队列中移除任务,true表示是,false表示否,默认false
 */
private volatile boolean removeOnCancel = false;

/**
 * 全局任务序号,当延迟时间相同时,序号小的先出队列,ScheduledFutureTask的sequenceNumber就是从这里获取的
 * 使用AtomicLong包装一个volatile long值,保证了原子性和线程安全,保证获取的任务序号不会重复
 */
private static final AtomicLong sequencer = new AtomicLong();

3 ScheduledFutureTask内部类

ScheduledFutureTask是ScheduledThreadPoolExecutor自己实现的专用延迟任务类型,也作为DelayedWorkQueue任务队列的任务实际类型。

直接继承了FutureTask,具有FutureTask的特性,表示ScheduledFutureTask也是异步执行结果类,同时可以接受Runnable和Callable类型的任务。

直接实现了RunnableScheduledFuture接口,也作为该接口的唯一实现类。该接口具有一个重要的抽象方法isPeriodic,用于判断该任务是否是周期性任务,在DelayedWorkQueue中就是通过该isPeriodic方法判断任务是否是周期性任务的。

RunnableScheduledFuture还继承了ScheduledFuture接口,ScheduledFuture接口表示一种延迟的异步执行结果,而ScheduledFuture接口又继承了Delayed顶级接口,Delayed接口用来描述那些应该在给定延迟时间之后执行的对象,它具有一个重要的抽象方法getDelay,用来返回与此接口关联的对象的剩余延迟时间,在DelayedWorkQueue中线程的阻塞时间就是通过该getDelay方法计算出来的。Delayed则是实现了 接口,它具有一个重要的抽象方法compareTo,用来比较大小,在DelayedWorkQueue中加入任务时就是通过该compareTo方法比较延迟时间长短并排序的。

在这里插入图片描述

ScheduledFutureTask 内部还有一个period 属性用来表示任务的类型:period=0, 说明当前任务是一次性的任务,执行完毕后就退出了;period 为负数,说明当前任务为fixed-delay 任务,是固定延迟的定时可重复执行任务,-perid就是scheduleWithFixedDelay方法的delay参数;period 为正数,说明当前任务为fixed-rate 任务, 是固定频率的定时可重复执行任务,period就是scheduleAtFixedRate方法的period 参数。

heapIndex属性记录所在延迟队列(底层数组)位置的索引,用于支持更快的执行cancel取消任务;time属性记录任务延迟时间纳秒;sequenceNumber属性记录任务被添加的序号,用于记录添加的先后顺序,在time相同时,sequenceNumber越小的越先执行。

ScheduledFutureTask内部的属性、构造器,以及getDelay、compareTo、cancel、isPeriodic方法源码如下:

/**
 * ScheduledThreadPoolExecutor内部的专用延迟任务
 */
private class ScheduledFutureTask<V>
        extends FutureTask<V> implements RunnableScheduledFuture<V> {

    /**
     * 任务被添加的序号,用于记录添加的先后顺序,在time相同时,sequenceNumber越小的越先执行
     */
    private final long sequenceNumber;

    /**
     * 任务延迟执行时间点纳秒
     */
    private long time;

    /**
     * period属性用来表示任务的类型,有以下三种:
     * period=0,说明当前任务是一次性的任务,执行完毕后就退出了。
     * period 为负数,说明当前任务为fixed-delay 任务,是固定延迟的定时可重复执行任务。
     * period 为正数,说明当前任务为fixed-rate 任务, 是固定频率的定时可重复执行任务。
     */
    private final long period;

    /**
     * 要重新执行的任务,仅被reExecutePeriodic方法调用,初始化为this
     */
    RunnableScheduledFuture<V> outerTask = this;

    /**
     * 所在延迟队列(底层数组)位置的索引,用于支持更快的执行cancel取消任务
     */
    int heapIndex;

    /**
     * 创建一个在指定纳秒时间之后执行的一次性任务,具有指定返回结果
     *
     * @param ns     任务延迟执行时间点纳秒
     * @param r      任务
     * @param result 指定返回结果
     */
    ScheduledFutureTask(Runnable r, V result, long ns) {
        //调用父类FutureTask的构造器
        super(r, result);
        //任务延迟执行时间点纳秒
        this.time = ns;
        //period设为0
        this.period = 0;
        //任务序号
        this.sequenceNumber = sequencer.getAndIncrement();
    }


    /**
     * 创建给定的指定纳秒时间之后执行的周期任务,具有指定返回结果
     *
     * @param r      任务
     * @param result 指定返回结果
     * @param ns     任务延迟执行时间点纳秒
     * @param period 任务类型
     */
    ScheduledFutureTask(Runnable r, V result, long ns, long period) {
        //调用父类FutureTask的构造器
        super(r, result);
        //任务延迟执行时间点纳秒
        this.time = ns;
        this.period = period;
        //任务序号
        this.sequenceNumber = sequencer.getAndIncrement();
    }

    /**
     * 创建一个在指定纳秒时间之后执行的一次性任务,具有指定返回结果
     *
     * @param callable 任务
     * @param ns       任务延迟执行时间点纳秒
     */
    ScheduledFutureTask(Callable<V> callable, long ns) {
        //调用父类FutureTask的构造器
        super(callable);
        //任务延迟执行时间点纳秒
        this.time = ns;
        //period设为0
        this.period = 0;
        //任务序号
        this.sequenceNumber = sequencer.getAndIncrement();
    }

    /**
     * 获取剩余延迟时间
     *
     * @param unit 时间单位
     * @return 剩余延迟时间
     */
    public long getDelay(TimeUnit unit) {
        //任务延迟执行时间点纳秒减去当前时间点纳秒,大于0表示还有延迟时间少于等于0则表示可以执行
        return unit.convert(time - now(), NANOSECONDS);
    }

    /**
     * 比较大小,即比较任务延迟执行时间点纳秒time以及任务序号sequenceNumber
     *
     * @param other 队列中的其他任务
     * @return 0 相等; -1 当前任务先被执行; 1 当前任务后被执行
     */
    public int compareTo(Delayed other) {
        //如果就是当前任务,那么返回0
        if (other == this) // compare zero if same object
            return 0;
        //如果不是同一个任务且属于ScheduledFutureTask类型,那么继续比较
        if (other instanceof ScheduledFutureTask) {
            //转换为ScheduledFutureTask类型
            ScheduledFutureTask<?> x = (ScheduledFutureTask<?>) other;
            //获取当前任务延迟执行时间点纳秒和指定任务的对应值的差diff
            long diff = time - x.time;
            //diff小于0,说明当前任务延迟执行时间点纳秒更小,更先被执行,返回-1
            if (diff < 0)
                return -1;
                //diff大于0,说明当前任务延迟执行时间点纳秒更大,更后被执行,返回1
            else if (diff > 0)
                return 1;
                //diff等于0,比较sequenceNumber,如果当前序号更小,那么更先被执行,返回-1
            else if (sequenceNumber < x.sequenceNumber)
                return -1;
                //否则更后被执行,返回1
            else
                return 1;
        }
        //其它类型,那么比较getDelay方法的返回值
        long diff = getDelay(NANOSECONDS) - other.getDelay(NANOSECONDS);
        return (diff < 0) ? -1 : (diff > 0) ? 1 : 0;
    }

    /**
     * 尝试取消任务
     *
     * @param mayInterruptIfRunning 如果应该中断正在执行此任务的线程,则为 true;否则允许正在运行的任务运行完成
     * @return 如果无法取消任务,则返回 false,这通常是由于它已经正常完成或者已取消;否则返回 true
     * 返回true也不代表任务方法没有执行完毕,有可能任务执行完了,只是不能获取返回值而已。
     */
    public boolean cancel(boolean mayInterruptIfRunning) {
        //调用父类的cancel方法,返回cancelled,即任务状态改变了
        boolean cancelled = super.cancel(mayInterruptIfRunning);
        //如果 cancelled为true,表示尝试取消成功
        //并且 removeOnCancel为true,removeOnCancel属性表示取消任务的时候是否从队列中移除任务,true表示移除
        //并且 当前任务的heapIndex大于等于
        if (cancelled && removeOnCancel && heapIndex >= 0)
            //那么从任务队列中移除任务
            remove(this);
        //返回父类方法的调用结果cancelled
        return cancelled;
    }


    /**
     * 是否是周期性任务
     *
     * @return true 是 false 否
     */
    public boolean isPeriodic() {
        //如果period不为0,那么就是周期任务
        return period != 0;
    }

    //其他方法后面讲
}

4 DelayedWorkQueue内部类

DelayedWorkQueue是ScheduledThreadPoolExecutor自己实现的专用延迟无界阻塞任务队列,内部同样采用数组作为存放任务的容器,逻辑上实现小顶堆,这完全可以类比DelayQueue 和PriorityBlockingQueue,区别是每一个元素(任务)会将自己在数组中的索引记录在自己内部heapIndex属性中,这消除了在取消某个任务时查找任务在数组中位置的过程(从O(log n)到O(1)),大大加快了删除速度。

所有堆操作都必须记录索引更改,主要在 siftUp 和 siftDown 方法中。删除任务后,任务的堆索引设置为 -1。需要注意的是,计划未来任务最多可以在队列中出现一次(对于其他类型的任务或工作队列这是不需要的),因此需要由heapIndex 进行唯一标识。

DelayedWorkQueue对内部任务ScheduledFutureTask的time属性进行小顶堆排序,即延迟时间,而如果两个任务的time延迟执行时间相同,那么先提交的任务将被先执行(比较sequenceNumber,越小说明越先被提交,该属性不会重复)。

从下面的内部属性可以看到,其原理和DelayQueue基本一致,在此原理不在赘述!

/**
 * ScheduledThreadPoolExecutor内部的专用延迟无界阻塞任务队列
 */
static class DelayedWorkQueue extends AbstractQueue<Runnable>
        implements BlockingQueue<Runnable> {
    /**
     * 初始容量
     */
    private static final int INITIAL_CAPACITY = 16;
    /**
     * 底层数组,初始容量为16
     */
    private RunnableScheduledFuture<?>[] queue =
            new RunnableScheduledFuture<?>[INITIAL_CAPACITY];

    /**
     * lock用于保证线程安全,生产和消费都需要获取同一个锁,创建对象实例的时候就初始化
     */
    private final ReentrantLock lock = new ReentrantLock();
    private int size = 0;

    /**
     * 类似于DelayQueue的Leader-Follower线程模型
     * 成为leader的线程将会在available条件变量上等待此时队列头结点的剩余延迟时间
     * 其他线程作为Follower在available条件变量上一直等待,直到被唤醒或中断
     * leader线程苏醒之后会将leader变量置空,在获取到元素之后最后会唤醒一个在available上等待的Follower线程
     * Leader-Follower线程模型可以避免没有必要的自旋或者没必要的唤醒
     */
    private Thread leader = null;

    /**
     * 一个条件变量available,用于消费者线程的等待和唤醒,创建对象实例的时候就初始化
     * 生产线程不会等待,因为队列是“无界”的,可以一直入队。
     */
    private final Condition available = lock.newCondition();

}

5 ScheduledThreadPoolExecutor的构造器

public ScheduledThreadPoolExecutor(int corePoolSize)

使用给定corePoolSize创建一个新 ScheduledThreadPoolExecutor。

public ScheduledThreadPoolExecutor(int corePoolSize,ThreadFactory threadFactory)

使用给定的corePoolSize和threadFactory创建一个新 ScheduledThreadPoolExecutor。

public ScheduledThreadPoolExecutor(int corePoolSize,RejectedExecutionHandler handler)

使用给定corePoolSize和handler创建一个新 ScheduledThreadPoolExecutor。

public ScheduledThreadPoolExecutor(int corePoolSize,ThreadFactory threadFactory, RejectedExecutionHandler handler)

使用给定corePoolSize和threadFactory和handler创建一个新 ScheduledThreadPoolExecutor。

上面的构造器最终都是调了父类的构造器,可以发现,只能指定corePoolSize和threadFactory和handler这三个参数,其他的参数都是ScheduledThreadPoolExecutor帮我们指定的,虽然还是可以在后面使用相关方法修改,但是建议不要轻易去这么做!

默认maximumPoolSize为Integer.MAX_VALU,默认超时时间为0,默认阻塞队列为DelayedWorkQueue

public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
            new DelayedWorkQueue());
}

public ScheduledThreadPoolExecutor(int corePoolSize,
                                   ThreadFactory threadFactory) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
            new DelayedWorkQueue(), threadFactory);
}

public ScheduledThreadPoolExecutor(int corePoolSize,
                                   RejectedExecutionHandler handler) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
            new DelayedWorkQueue(), handler);
}

public ScheduledThreadPoolExecutor(int corePoolSize,
                                   ThreadFactory threadFactory,
                                   RejectedExecutionHandler handler) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
            new DelayedWorkQueue(), threadFactory, handler);
}

6 schedule一次性任务

public < V > ScheduledFuture< V > schedule(Callable< V > callable, long delay, TimeUnit unit)
public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit)

command和callable都表示线程任务,delay表示从现在开始延迟执行的时间,unit表示延迟参数的时间单位。schedule方法创建并执行在给定延迟时间后启用的一次性任务操作,返回一个ScheduledFuture延时异步结果对象,实际类型就是队伍对应的ScheduledFutureTask。

另外,execute和submit系列方法都被重写为内部调用schedule方法,并且delay都为0,即不需要延迟执行的一次性任务。

public void execute(Runnable command) {
    schedule(command, 0, NANOSECONDS);
}

public Future<?> submit(Runnable task) {
    return schedule(task, 0, NANOSECONDS);
}

public <T> Future<T> submit(Runnable task, T result) {
    return schedule(Executors.callable(task, result), 0, NANOSECONDS);
}

public <T> Future<T> submit(Callable<T> task) {
    return schedule(task, 0, NANOSECONDS);
}

两个schedule方法,一个传递Runnable一个传递Callable,Runnable会被Executors.callable包装成为Callable,除此之外它们的源码一致。

/**
 * @param command Runnable类型的任务
 * @param delay   从现在开始延迟执行的时间
 * @param unit    时间单位
 * @return 与任务关联的ScheduledFutureTask对象
 */
public ScheduledFuture<?> schedule(Runnable command,
                                   long delay,
                                   TimeUnit unit) {
    //callable和unit的null校验
    if (command == null || unit == null)
        throw new NullPointerException();
    //调用decorateTask对任务进行控制,可由子类重写
    //默认返回一个新建的ScheduledFutureTask
    RunnableScheduledFuture<?> t = decorateTask(command,
            new ScheduledFutureTask<Void>(command, null,
                    triggerTime(delay, unit)));
    delayedExecute(t);
    return t;
}

/**
 * 修改或替换用于执行可调用的任务。此方法可用于子类重写用于管理内部任务的具体类。 默认实现仅返回给定的任务。
 *
 * @param runnable 提交的线程任务
 * @param task     延迟异步执行结果
 * @return 延迟异步执行结果
 * @since 1.6
 */
protected <V> RunnableScheduledFuture<V> decorateTask(
        Runnable runnable, RunnableScheduledFuture<V> task) {
    return task;
}


/**
 * @param callable Runnable类型的任务
 * @param delay    从现在开始延迟执行的时间
 * @param unit     时间单位
 * @return 与任务关联的ScheduledFutureTask对象
 */
public <V> ScheduledFuture<V> schedule(Callable<V> callable,
                                       long delay,
                                       TimeUnit unit) {
    //callable和unit的null校验
    if (callable == null || unit == null)
        throw new NullPointerException();
    //调用decorateTask对任务进行控制,可由子类重写
    //默认返回一个新建的ScheduledFutureTask
    RunnableScheduledFuture<V> t = decorateTask(callable,
            new ScheduledFutureTask<V>(callable,
                    triggerTime(delay, unit)));
    delayedExecute(t);
    return t;
}

/**
 * 修改或替换用于执行可调用的任务。此方法可用于子类重写用于管理内部任务的具体类。 默认实现仅返回给定的任务。
 *
 * @param callable 提交的线程任务
 * @param task     延迟异步执行结果
 * @return 延迟异步执行结果
 * @since 1.6
 */
protected <V> RunnableScheduledFuture<V> decorateTask(
        Callable<V> callable, RunnableScheduledFuture<V> task) {
    return task;
}

默认情况下,传入的Runnable和Callable都会被包装成为一个ScheduledFutureTask,那么在线程池中执行的run方法就是ScheduledFutureTask的run方法。

6.1 triggerTime任务触发时间点

在调用ScheduledFutureTask构造器的时候,调用了一个triggerTime方法,该方法从当前时间点开始,根据延迟时间和时间单位返回延迟操作的触发时间点纳秒,就是计算time属性的值!

有意思的是,队头任务(最近将被执行的任务)和将要新提交的任务的执行间隔时间不能超过long类型的最大值,否则在比较延迟时间长短的时候会由于数值溢出而出现问题,因此这里实际设置的延迟时间不一定是参数中的时间!

/**
 * 从当前时间点开始
 * 根据延迟时间和时间单位返回延迟操作的触发时间点纳秒
 *
 * @param delay 延迟时间
 * @param unit  时间单位
 * @return 延迟操作的触发时间点纳秒
 */
private long triggerTime(long delay, TimeUnit unit) {
    //将延迟时间转换为纳秒值,调用另一个triggerTime方法,
    return triggerTime(unit.toNanos((delay < 0) ? 0 : delay));
}

/**
 * 从当前时间点开始
 * 根据延迟时间纳秒返回延迟操作的触发时间点纳秒
 *
 * @param delay 延迟时间纳秒
 * @return 延迟操作的触发时间点纳秒
 */
long triggerTime(long delay) {
    //now()返回当前时间纳秒
    //如果delay小于long类型最大值的一半,那么就是 当前时间纳秒 + 延迟时间纳秒
    //否则 将队列中所有任务延迟的最大差值约束在Long.MAX_VALUE,以避免在比较中溢出。
    return now() +
            ((delay < (Long.MAX_VALUE >> 1)) ? delay : overflowFree(delay));
}

/**
 * 返回当前时间点纳秒
 */
final long now() {
    return System.nanoTime();
}

/**
 1. 将队列中所有任务延迟的最大差值约束在Long.MAX_VALUE,以避免在比较中溢出。
 */
private long overflowFree(long delay) {
    //获取但不移除队列头结点head
    Delayed head = (Delayed) super.getQueue().peek();
    //如果head不为null,说明有任务
    if (head != null) {
        //获取head的剩余超时时间纳秒headDelay
        long headDelay = head.getDelay(NANOSECONDS);
        //如果headDelay小于0表示队头任务已过期但是并没有执行
        //并且delay-headDelay小于0,这说明队头和队尾结点(当前新增任务)的延迟时间差值已经超出了long类型的范围
        if (headDelay < 0 && (delay - headDelay < 0))
            //那么delay重设置为Long.MAX_VALUE + headDelay,这样保证队头和队尾任务间隔时间在long类型范围之类
            delay = Long.MAX_VALUE + headDelay;
    }
    //如果head为null,直接返回原值
    return delay;
}

6.2 delayedExecute延迟/定期执行核心方法

schedule方法最后都调用了delayedExecute方法,该方法就是用于延迟或定期执行任务的方法,也是ScheduledThreadPoolExecutor的核心方法。

大概步骤为:

  1. 如果线程池已关闭(非RUNNING状态),直接执行拒绝策略;
  2. 否则,尝试执行:
    1. 首先将task任务通过DelayedWorkQueue的add方法加入到阻塞队列。
    2. 加入队列之后,继续判断。如果线程池已关闭(非RUNNING状态),并且当前状态不能继续运行,那么尝试从队列移除task,如果这个三个条件判断都满足,那么调用cancel取消任务;
    3. 否则,说明该任务支持执行。那么调用ensurePrestart确保至少有一条线程,能够执行任务。
/**
 * 延迟或定期任务的主要执行方法。
 * 如果池已关闭,则拒绝该任务。否则,将任务添加到队列中,并启动线程(如有必要)以运行它。
 * (我们无法预启动线程以运行任务,因为任务(可能)尚不应运行。 如果在添加任务时关闭池,则取消并删除它(如果状态和运行后关闭参数)。
 *
 * @param task 被包装的RunnableScheduledFuture任务
 */
private void delayedExecute(RunnableScheduledFuture<?> task) {
    /*如果线程池已关闭(非RUNNING状态),直接执行拒绝策略*/
    if (isShutdown())
        //执行拒绝策略
        reject(task);
    /*否则,尝试执行*/
    else {
        /*
         * 首先将task任务通过DelayedWorkQueue的add方法加入到阻塞队列,该方法就是通过新元素构建小顶堆的逻辑,以及Leader-Follower线程模型的应用。
         * 关于小顶堆以及以及Leader-Follower线程模型,我们以前在PriorityBlockingQueue和DelayQueue中就讲过了,远离都差不多,在此不再赘述。
         * 需要和注意的是,在最终构建完成之后,这个task以及其他移动了位置的task还会保存或者更新自己在数组中的索引位置,方便后续查找移除
         */
        super.getQueue().add(task);
        /*
         * 加入队列之后,继续判断
         * 如果线程池已关闭(非RUNNING状态),并且当前状态不能继续运行,那么尝试从队列移除task成功
         * 三个条件判断都满足,那么调用cancel取消任务
         */
        if (isShutdown() &&
                !canRunInCurrentRunState(task.isPeriodic()) &&
                remove(task))
            //移除task成功之后,尝试调用task本身的cancel方法取消这个任务
            task.cancel(false);
            /*
             * 否则,说明该任务支持执行,那么调用ensurePrestart确保至少有一条线程,能够执行任务
             */
        else
            ensurePrestart();
    }
}

6.2.1 canRunInCurrentRunState是否可执行

canRunInCurrentRunState判断某个任务在关闭线程池(SHUTDOWN状态)之后是否还可以继续运行。周期任务是通过continueExistingPeriodicTasksAfterShutdown属性控制的,而一次性任务是通过executeExistingDelayedTasksAfterShutdown属性控制的。

/**
 * 判断某个任务在关闭线程池(SHUTDOWN状态)之后是否还可以继续运行
 *
 * @param periodic 如果此任务是周期性任务,则为 true;一次性任务则为false
 * @return true 应该 false 不应该
 */
boolean canRunInCurrentRunState(boolean periodic) {
    //调用isRunningOrShutdown方法
    // 如果periodic为true,那么传递continueExistingPeriodicTasksAfterShutdown属性
    // 如果periodic为false,那么传递executeExistingDelayedTasksAfterShutdown属性
    return isRunningOrShutdown(periodic ?
            continueExistingPeriodicTasksAfterShutdown :
            executeExistingDelayedTasksAfterShutdown);
}

/**
 * 该任务是否可以继续运行
 *
 * @param shutdownOK 如果在SHUTDOWN状态还可以运行,则为true,否则就是false
 * @return true 应该 false 不应该
 */
final boolean isRunningOrShutdown(boolean shutdownOK) {
    //获取线程池状态
    int rs = runStateOf(ctl.get());
    //如果是RUNNING,或者是SHUTDOWN并shutdownOK为true,那么返回true
    return rs == RUNNING || (rs == SHUTDOWN && shutdownOK);
}

6.2.2 ensurePrestart确保Worker数量

在确定可以执行给定的任务之后,最后会调用ensurePrestart方法,该方法确保线程池中开启一个工作线程,即使corePoolSize=0,用于保证任务能够被执行。

有趣的是,该方法被实现在父类ThreadPoolExecutor中,但是在JDK1.8中只有子类ScheduledThreadPoolExecutor调用。

/**
 1. 该方法是父类ThreadPoolExecutor中的方法,和prestartCoreThread方法类似,但是又有区别
 2. 尝试启动一个核心线程,使其处于等待工作的空闲状态或者去队列执行任务。
 3. 如果已启动所有核心线程,此方法不启动。如果核心线程数被设置为0,那么还是要启动一条线程
 */
void ensurePrestart() {
    //当前线程数wc
    int wc = workerCountOf(ctl.get());
    //如果小于corePoolSize,那么调用父类addWorker方法启动一条个核心线程
    if (wc < corePoolSize)
        addWorker(null, true);
        /*否则,如果wc==0,即核心线程被设置为0,还是需要调用addWorker启动一条线程*/
    else if (wc == 0)
        addWorker(null, false);
}

6.3 ScheduledFutureTask.run执行任务

在ScheduledThreadPoolExecutor的runWorker方法中调用的task.run()实际上就是调用的ScheduledFutureTask的run方法。该方法就是执行具体任务以及实现周期性控制的核心方法。

run方法实际上比较简单,大概步骤为:

  1. 使用periodic表示当前任务是否是周期性任务,true 是 false 否;
  2. 在执行真正的任务之前,判断当前任务是否可以执行,如果不能执行,那么cancel取消该任务。
  3. 否则,表示可以执行。继续判断是否是一次性任务。如果是一次性任务,那么调用ScheduledFutureTask的父类FutureTask的run方法执行任务而在FutureTask的run方法中,有会调用c.call()方法,这里面才是我们自己写的任务逻辑。run方法完毕则任务结束。
  4. 否则,表示周期性任务。那么调用父类FutureTask的runAndReset方法执行任务并且执行完毕之后重置任务状态。执行并重置失败之后,退出run方法,该任务将不再执行;执行并重置成功之后,进入if代码块:
    1. 调用setNextRunTime设置任务下一次要执行的时间点。
    2. 调用reExecutePeriodic将下一次待执行的任务放置到DelayedWorkQueue中,等待下一次执行,这个outerTask默认指向该任务自己。
/**
 * ScheduledFutureTask的run方法
 */
public void run() {
    //periodic表示当前任务是否是周期性任务,true 是 false 否
    boolean periodic = isPeriodic();
    /*
     * 在执行真正的任务之前,判断当前任务是否可以执行
     * 如果不能执行,那么cancel取消该任务
     */
    if (!canRunInCurrentRunState(periodic))
        cancel(false);
        /*
         * 否则,表示可以执行。继续判断是否是一次性任务
         * 如果是一次性任务,那么调用ScheduledFutureTask的父类FutureTask的run方法执行任务
         * 而在FutureTask的run方法中,有会调用c.call()方法,这里面才是我们自己写的任务逻辑
         */
    else if (!periodic)
        ScheduledFutureTask.super.run();
        /*
         * 否则,表示周期性任务。
         * 那么调用父类FutureTask的runAndReset方法执行任务并且执行完毕之后重置任务
         */
    else if (ScheduledFutureTask.super.runAndReset()) {
        //执行并重置成功之后,设置任务下一次要执行的时间点
        setNextRunTime();
        //下一次待执行的任务放置到DelayedWorkQueue中,这个outerTask默认指向该任务自己
        reExecutePeriodic(outerTask);
    }
}

6.3.1 runAndReset运行并重置任务

如果是一次性任务,那么调用父类FutureTask的run方法即可,执行完毕之后该任务就没了;如果是周期任务,那么调用父类FutureTask的runAndReset方法执行任务并且执行完毕之后重置任务。

runAndReset和我们前面讲的run方法非常相似,区别就是不会设置结果(正确的结果)。在最后会判断如果状态还是NEW,那么说明“重置”成功,实际上是在该方法的代码中根本就没有改变过状态值,这里是为了防止被取消的逻辑。那么现在我们能够明白,这里所谓的“重置”,那就是任务状态一直是NEW而已,即没有改变任务状态的时候,任务就可以重复运行!

在任务抛出异常的时候,会设置异常结果值,并且该周期方法将不再执行!

/**
 * 在不设置结果的情况下执行计算,然后将此将来重置为初始状态,如果计算遇到异常或被取消,则无法继续执行。
 * 类似于run,多了reset逻辑
 *
 * @return 如果计算-重置成功,则返回true,否则返回false
 */
protected boolean runAndReset() {
    /*
     * 如果state不是NEW状态
     * 或者 state是NEW状态,但是CAS的将runner从null设置为当前调用线程失败
     *
     * 以上两个条件满足一个就立即返回,任务不会被执行,即要求任务在此前没有被执行过才能开始执行
     */
    if (state != NEW ||
            !UNSAFE.compareAndSwapObject(this, runnerOffset,
                    null, Thread.currentThread()))
        //直接返回false
        return false;
    //ran用来保存任务是否执行成功
    boolean ran = false;
    //s保存此时的state值
    int s = state;
    try {
        //保存要执行的任务c
        Callable<V> c = callable;
        //如果c不为null,并且state还是为NEW,那么可以执行真正的任务
        //这里还需要校验一下,因为在上次校验到此之间可能存在其他线程取消了任务
        //如果被取消了,那么不会执行底层真正的任务
        if (c != null && s == NEW) {
            try {
                //调用c.call()方法,这里才是执行真正的任务,这个call方法中的代码就是我们自己编写的代码
                //如果call执行成功,那么也不会设置返回值result
                c.call(); // don't set result
                //ran置为true,表示执行成功
                ran = true;
            } catch (Throwable ex) {
                //如果call方法抛出异常,调用setException方法,用于设置异常的返回值,并唤醒因为get阻塞的线程
                setException(ex);
            }
        }
    } finally {
        //执行完毕后在finally中,将runner设置为null,到此表示任务执行完毕
        //在任务执行过程中runner必须一直非null,用来保证run()方法不会被并发的调用
        runner = null;
        //重新获取此时的state,因为在任务执行过程中可能被中断,state会被改变
        s = state;
        /*
         * 如果状态大于等于INTERRUPTING,表示任务执行过程中其他线程执行了cancel(true)成功
         * 那么本次任务执行的setException以及set方法都执行失败,但是此执行线程不一定被中断了
         * 可能执行线程在执行任务call()方法完毕时,cancel的线程更改了state值为INTERRUPTING,这将导致执行线程的setException或者set失败
         *
         * 随后执行线程将runner置空,那么cancel线程在后续代码中由于runner为null将不会中断执行线程
         * 但是也有可能cancel线程先获取到了runner,此时runner还不为null,但是执行线程实际上将任务执行完了,此时还是会中断线程
         */
        if (s >= INTERRUPTING)
            //处理中断,等待任务状态变成INTERRUPTED状态,即等待执行线程被中断或者不被中断
            handlePossibleCancellationInterrupt(s);
    }
    //如果ran为true,即任务执行成功
    //并且任务状态还是为NEW,表示重置成功(实际上是在该方法的代码中根本就没有改变过状态值,这里是为了防止被取消的逻辑),那么就返回true
    return ran && s == NEW;
}

6.3.2 setNextRunTime设置下次运行时间点

在周期任务执行并重置完毕之后,会设置周期任务下一次运行的时间点。scheduleAtFixedRate和scheduleWithFixedDelay方法内部会调用到该方法,许多人分不清这两个方法的区别,实际上该方法的源码已经明确指出了它们的区别!

/**
 * ScheduledFutureTask中的方法
 * scheduleAtFixedRate和scheduleWithFixedDelay方法内部会调用到该方法
 * 设置周期任务下一次运行的时间点,这里就是这两个方法的区别
 */
private void setNextRunTime() {
    //获取period
    long p = period;
    /*
     * 如果p>0,说明当前任务为fixed-rate 任务,是固定频率的定时可重复执行任务。
     * p就是scheduleAtFixedRate (Runnable command, long initialDelay, long period, TimeUnit unit)方法传递的period参数
     * p代表 前一个任务开始之后,每隔 period 时间之后重复执行
     * 如果一次任务执行时间小于period计划时间,那么任务开始间隔就是initialDelay、initialDelay+period、initialDelay + 2 * period……。
     * 如果一次任务执行时间大于等于period计划时间,下一次任务执行将在当前任务执行结束之后立即执行。
     * fixed-rate 任务在同一时间不会有多个线程同时执行。
     */
    if (p > 0)
        //可以看到,下一次任务的执行时间点就是基于 上一次任务开始执行时间向后延迟p
        time += p;
        /*
         * 否则,p<0,说明当前任务为fixed-delay 任务,是固定延迟的定时可重复执行任务。
         * -p就是scheduleWithFixedDelay (Runnable, long initialDelay, long delay, TimeUnit timeunit)方法传递的delay参数
         * -p(delay)代表 前一个任务执行的结束和下一个执行的开始之间的间隔(fixed-delay 任务)。
         * 计划任务在同一时间不会有多个线程同时执行
         *
         * fixed-delay 任务在同一时间不会有多个线程同时执行。
         */
    else
        //可以看到,下一次任务的执行时间点就是基于 当前时间(任务执行完毕时间)向后延迟-p
        time = triggerTime(-p);
}

6.3.3 reExecutePeriodic重新提交周期任务

在周期任务执行并重置完毕并且设置好下一次运行的时间点之后,最后的步骤就是再次把任务丢到任务队列中去。类似于delayedExecute方法,重新执行周期任务,区别是不会再断度判断isShutdown或者执行拒绝策略。

/**
 * ScheduledThreadPoolExecutor 中的方法
 * 类似于delayedExecute方法,重新执行周期任务,区别是不会再断度判断isShutdown或者执行拒绝策略
 *
 * @param task the task
 */
void reExecutePeriodic(RunnableScheduledFuture<?> task) {
    //如果当前状态可以执行该周期任务
    if (canRunInCurrentRunState(true)) {
        //将该任务添加到任务队列
        super.getQueue().add(task);
        /*
         * 如果当前不支持执行该周期任务,并且从队列移除该任务成功
         * 两个条件都满足,那么调用cancel取消任务
         */
        if (!canRunInCurrentRunState(true) && remove(task))
            //移除task成功之后,尝试调用task本身的cancel方法取消这个任务
            task.cancel(false);
            /*
             * 否则,说明该任务支持执行,那么调用ensurePrestart确保至少有一条线程,能够执行任务
             */
        else
            ensurePrestart();
    }
}

6.4 测试案例

使用工具类创建内置5个核心线程的ScheduledExecutorService,随后调用schedule() 方法传递一个任务,后边的两个参数定义了这个任务将在5秒钟之后被执行。

/**
 * @author lx
 */
public class ScheduledThreadPoolExecutorTest2 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ScheduledExecutorService scheduledExecutorService =
                Executors.newScheduledThreadPool(5);
        ScheduledFuture scheduledFuture = scheduledExecutorService.schedule(() -> {
            System.out.println("Executed!");
            return "Called";
        }, 5, TimeUnit.SECONDS);
        System.out.println(scheduledFuture.get());
        scheduledExecutorService.shutdown();
    }
}

7 scheduleWithFixedDelay固定周期任务

public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)
command:表示要执行的Runnable类型的任务;
initialDelay:表示提交任务后延迟多少时间开始执行command;
delay:表示当任务执行完毕后延长多少时间后再次运行command;
unit:表示initialDelay 和delay 的时间单位。

该任务将会在首个initialDelay时间延迟之后得到执行,然后在前一个任务开始结束后,尝试每隔 delay时间之后重复执行,直到任务运行中抛出了异常,被取消了,或者关闭了线程池(SHUTDOWN状态之后)。

该方法中delay作为前一个任务执行结束和下一个任务执行的开始之间的间隔。即如果1秒后开始执行第一次任务,任务耗时5秒,任务间隔时间3秒,那么第二次任务执行的时间是在第10秒开始。周期任务在同一时间不会有多个线程同时执行。

关于周期性控制的源码,在上面的setNextRunTime部分。

/**
 * @param command      表示要执行的Runnable类型的任务
 * @param initialDelay 表示提交任务后延迟多少时间开始执行command
 * @param delay        表示当任务执行完毕后延长多少时间后再次运行command
 * @param unit         表示initialDelay 和delay 的时间单位
 * @return 与任务关联的ScheduledFutureTask对象
 */
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                 long initialDelay,
                                                 long delay,
                                                 TimeUnit unit) {
    //callable和unit的null校验
    if (command == null || unit == null)
        throw new NullPointerException();
    //如果delay小于等于0,那么抛出IllegalArgumentException异常
    if (delay <= 0)
        throw new IllegalArgumentException();

    ScheduledFutureTask<Void> sft =
            new ScheduledFutureTask<Void>(command,
                    null,
                    triggerTime(initialDelay, unit),
                    //注意这里是-delay,是负数
                    unit.toNanos(-delay));
    //调用decorateTask对任务进行控制,可由子类重写
    //默认返回一个新建的ScheduledFutureTask
    RunnableScheduledFuture<Void> t = decorateTask(command, sft);
    //指向t,默认就是指向自己
    sft.outerTask = t;
    //调用同一个方法
    delayedExecute(t);
    return t;
}

7.1 测试案例

/**
 * @author lx
 */
public class ScheduleWithFixedDelayTest {
    public static void main(String[] args) {
        ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(1);

        System.out.println("测试scheduleWithFixedDelay");
        System.out.println("---首次延迟1秒执行后,前一个任务结束3秒后再次执行,任务耗时1秒---" + Calendar.getInstance().get(Calendar.SECOND) + "\r\n");
        scheduledThreadPoolExecutor.scheduleWithFixedDelay(() -> {
            System.out.println("---开始执行---" + Calendar.getInstance().get(Calendar.SECOND));
            //假设任务耗时1秒
            LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
            System.out.println("---执行完毕---" + Calendar.getInstance().get(Calendar.SECOND) + "\r\n");
        }, 1, 3, TimeUnit.SECONDS);
    }
}

8 scheduleAtFixedRate固定频率任务

public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)
command:表示要执行的Runnable类型的任务;
initialDelay :表示提交任务后延迟多少时间开始执行command;
period:表示连续执行之间的周期;
unit :表示initialDelay 和period的时间单位。

该任务将会在首个initialDelay时间延迟之后得到执行,然后在前一个任务开始之后,尝试每隔 period 时间之后重复执行,直到任务运行中抛出了异常,被取消了,或者关闭了线程池(SHUTDOWN状态之后)。

如果任务执行时间小于period计划时间,那么任务开始间隔就是initialDelay、initialDelay+period、initialDelay + 2 * period……。如果任务执行时间大于等于period计划时间,下一次执行将在当前执行结束执行之后才会立即执行。周期任务在同一时间不会有多个线程同时执行。

关于周期性控制的源码,在上面的setNextRunTime部分。

/**
 * @param command      表示要执行的Runnable类型的任务
 * @param initialDelay 表示提交任务后延迟多少时间开始执行command
 * @param period       表示连续执行之间的周期
 * @param unit         表示initialDelay 和period的时间单位
 * @return 与任务关联的ScheduledFutureTask对象
 */
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                              long initialDelay,
                                              long period,
                                              TimeUnit unit) {
    //callable和unit的null校验
    if (command == null || unit == null)
        throw new NullPointerException();
    //如果period小于等于0,那么抛出IllegalArgumentException异常
    if (period <= 0)
        throw new IllegalArgumentException();
    ScheduledFutureTask<Void> sft =
            new ScheduledFutureTask<Void>(command,
                    null,
                    triggerTime(initialDelay, unit),
                    //这里是period,是正数
                    unit.toNanos(period));
    //调用decorateTask对任务进行控制,可由子类重写
    //默认返回一个新建的ScheduledFutureTask
    RunnableScheduledFuture<Void> t = decorateTask(command, sft);
    //指向t,默认就是指向自己
    sft.outerTask = t;
    //调用同一个方法
    delayedExecute(t);
    return t;
}

8.1 测试案例

/**
 * @author lx
 */
public class ScheduleAtFixedRateTest {
    static class ScheduleAtFixedRate1 {
        public static void main(String[] args) {

            ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(1);

            System.out.println("测试scheduleAtFixedRate,此时任务执行时间小于period计划时间");
            System.out.println("---首次延迟1秒执行后,每三秒执行一次,任务耗时1秒---" + Calendar.getInstance().get(Calendar.SECOND) + "\r\n");
            scheduledThreadPoolExecutor.scheduleAtFixedRate(() -> {
                System.out.println("---开始执行---" + Calendar.getInstance().get(Calendar.SECOND));
                //假设任务耗时1秒
                LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
                System.out.println("---执行完毕---" + Calendar.getInstance().get(Calendar.SECOND) + "\r\n");
            }, 1, 3, TimeUnit.SECONDS);
        }
    }

    static class ScheduleAtFixedRate2 {
        public static void main(String[] args) {
            ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(1);

            System.out.println("测试scheduleAtFixedRate,此时任务执行时间大于period计划时间");
            System.out.println("---首次延迟1秒执行后,每三秒执行一次,任务耗时4秒---" + Calendar.getInstance().get(Calendar.SECOND) + "\r\n");
            scheduledThreadPoolExecutor.scheduleAtFixedRate(() -> {
                System.out.println("---开始执行---" + Calendar.getInstance().get(Calendar.SECOND));
                //假设任务耗时4秒
                LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(4));
                System.out.println("---执行完毕---" + Calendar.getInstance().get(Calendar.SECOND) + "\r\n");
            }, 1, 3, TimeUnit.SECONDS);

        }
    }
}

9 设置线程池参数

除了父类ThreadPoolExecutor的一系列方法之外,还有自己的两个方法:

public void setContinueExistingPeriodicTasksAfterShutdownPolicy(boolean value)

关闭线程池之后(SHUTDOWN状态)是否继续执行周期任务,true表示是,false表示否,默认false。

public void setExecuteExistingDelayedTasksAfterShutdownPolicy(boolean value)

关闭线程池之后(SHUTDOWN状态)是否继续执行延迟一次性任务,true表示是,false表示否。此值默认为 true。

/**
 * 关闭线程池之后(SHUTDOWN状态)是否继续执行周期任务,默认false。
 *
 * @param value true表示是,false表示否
 */
public void setContinueExistingPeriodicTasksAfterShutdownPolicy(boolean value) {
    //设置值
    continueExistingPeriodicTasksAfterShutdown = value;
    //如果value为false 并且线程池非RUNNING状态
    if (!value && isShutdown())
        onShutdown();
}

/**
 * 关闭线程池之后(SHUTDOWN状态)是否继续执行延迟一次性任务,此值默认为 true。
 *
 * @param value true表示是,false表示否
 */
public void setExecuteExistingDelayedTasksAfterShutdownPolicy(boolean value) {
    //设置值
    executeExistingDelayedTasksAfterShutdown = value;
    //如果value为false 并且线程池非RUNNING状态
    if (!value && isShutdown())
        //取消并清除由于关闭策略而不应运行的所有队列中的任务。
        onShutdown();
}

/**
 * @return 线程池是不是RUNNING状态,true 不是RUNNING状态,false 是RUNNING状态
 */
public boolean isShutdown() {
    return !isRunning(ctl.get());
}

/**
 * 在shutdown方法中线程池变成SHUTDOWN之后被调用,或者改变两个取消策略的方法中被调用
 * 取消并清除由于关闭策略而不应运行的所有队列中的符号位情况的任务。
 */
@Override
void onShutdown() {
    //获取任务队列
    BlockingQueue<Runnable> q = super.getQueue();
    //获取关闭线程池之后(SHUTDOWN状态)是否继续执行延迟一次性任务的值keepDelayed
    boolean keepDelayed =
            getExecuteExistingDelayedTasksAfterShutdownPolicy();
    //获取关闭线程池之后(SHUTDOWN状态)是否继续执行周期任务的值keepDelayed
    boolean keepPeriodic =
            getContinueExistingPeriodicTasksAfterShutdownPolicy();
    /*如果两个都是false,那么全部任务队列中的任务都取消并移除*/
    if (!keepDelayed && !keepPeriodic) {
        /*遍历任务队列快照*/
        for (Object e : q.toArray())
            //cancel取消任务
            if (e instanceof RunnableScheduledFuture<?>)
                ((RunnableScheduledFuture<?>) e).cancel(false);
        //清理整个队列
        q.clear();
    }
    /*否则,那么任务队列中的任务根据实际情况取消并移除*/
    else {
        // Traverse snapshot to avoid iterator exceptions
        /*遍历任务队列快照*/
        for (Object e : q.toArray()) {
            if (e instanceof RunnableScheduledFuture) {
                RunnableScheduledFuture<?> t =
                        (RunnableScheduledFuture<?>) e;
                //t是否是周期任务,如果是那么!keepPeriodic,如果不是那么!keepDelayed
                //或者 t是否已经被取消了,那么仍然尝试移除任务队列
                if ((t.isPeriodic() ? !keepPeriodic : !keepDelayed) ||
                        t.isCancelled()) { // also remove if already cancelled
                    //如果移除队列成功
                    if (q.remove(t))
                        //cancel取消任务
                        t.cancel(false);
                }
            }
        }
    }
    //父类的方法,尝试彻底终止线程池
    tryTerminate();
}
  • 36
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 7
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

刘Java

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值