线程池 调度 java_Java调度线程池ScheduleExecutorService

作者: 一字马胡

转载标志 【2017-11-03】

更新日志日期更新内容备注2017-11-03添加转载标志持续更新如果在一个ScheduleExecutorService中提交一个任务,这个任务的调度周期设置

的时间比任务本身执行的时间短的话会出现什么情况?也就是在线程调度时间已经到了

但是上次的任务还没有做完的情况下,ScheduleExecutorService是怎么处理的?

这个问题曾经困扰了我很久,我们都知道,ScheduleExecutorService是一个支持周期调度的线程池,我们可以设置调度的周期period,ScheduleExecutorService会按照设定好的周期调度我们的任务,如果我们设定的调度周期小于任务运行时间,那么很好理解,比如说我们设置的调度周期为1秒,而任务实际只需要10毫秒就可以执行完成一次,那么执行完成之后放到调度队列即可,下次调度时间到了再次调度执行。那么,如果我们的任务执行时间大于我们设定的调度时间会怎么样?比如我们设定的调度周期为1秒,但是我们的任务每次需要执行2秒,这个情况是不是很奇怪呢?

对于ScheduleExecutorService来说,你给我设定的调度周期是1秒,那么我当然1秒就会去运行一次你,但是运行1秒后发现你还在运行,那我是再次运行你还是等你运行完成再调度你运行?

当然,这都是我的主观臆断来猜测ScheduleExecutorService的原理,ScheduleExecutorService的真正原理需要去阅读源码来理解,下面带着这个问题,以解决这个问题为目标去看一下ScheduleExecutorService的源码吧。

首先,我们使用下面的代码作为测试:private static Runnable blockRunner = () -> {        try {

TimeUnit.SECONDS.sleep(2);

System.out.println("one round:" + new Date());

} catch (InterruptedException e) {

e.printStackTrace();

}

};    private static ScheduledExecutorService scheduledExecutorService =

Executors.newScheduledThreadPool(2);    public static void main(String ... args) {

scheduledExecutorService

.scheduleAtFixedRate(blockRunner, 0, 100, TimeUnit.MILLISECONDS);

}

我们设定了调度周期为100毫秒,但是blockRunner实际上需要执行2秒才能返回。关于java的线程池,已经在前面的文章中写到了,可以参考下面的文章:

先来看一下scheduleAtFixedRate这个方法:public ScheduledFuture> scheduleAtFixedRate(Runnable command,                                                  long initialDelay,                                                  long period,

TimeUnit unit) {        if (command == null || unit == null)            throw new NullPointerException();        if (period <= 0)            throw new IllegalArgumentException();

ScheduledFutureTask sft =            new ScheduledFutureTask(command,                                          null,

triggerTime(initialDelay, unit),

unit.toNanos(period));

RunnableScheduledFuture t = decorateTask(command, sft);

sft.outerTask = t;

delayedExecute(t);        return t;

}

我们的任务command被包装了两次,一次变成了一个ScheduledFutureTask类型的对象,然后又变成了RunnableScheduledFuture类型的对象。然后执行了一个方法delayedExecute,这个方法字面意思上看起来像是延时执行的意思,看一下它的代码:private void delayedExecute(RunnableScheduledFuture> task) {        if (isShutdown())

reject(task);        else {            super.getQueue().add(task);            if (isShutdown() &&

!canRunInCurrentRunState(task.isPeriodic()) &&

remove(task))

task.cancel(false);            else

ensurePrestart();

}

}

它的执行逻辑是:如果线程池被关闭了,那么拒绝提交的任务,否则,将该任务添加队列中去。这个队列就是ThreadPoolExecutor中的workQueue,而这个workQueue是在ThreadPoolExecutor的构造函数中被初始化的,也就是下面这关键的一句:public ScheduledThreadPoolExecutor(int corePoolSize,

ThreadFactory threadFactory,

RejectedExecutionHandler handler) {        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,              new DelayedWorkQueue(), threadFactory, handler);

}

也就是说,我们的任务被添加到了一个DelayedWorkQueue队列中去了,而DelayedWorkQueue我们在Java阻塞队列详解中已经分析过,它是一个可以延迟消费的阻塞队列。而延时的时间是通过接口Delayed的getDelay方法来获得的,我们最后找到ScheduledFutureTask实现了Delayed的getDelay方法。public long getDelay(TimeUnit unit) {            return unit.convert(time - now(), NANOSECONDS);

}

time变量是什么?原来是delay,好像和period无关啊!!分析了这么久,发现这是第一次执行任务的逻辑啊,我想知道的是第二次、第三次以后和初始的delay无关之后的周期调度的情况啊,继续找吧!

然后发现了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);

}

}

}

最为关键的地方在于:else if (ScheduledFutureTask.super.runAndReset()) {

setNextRunTime();

reExecutePeriodic(outerTask);

}

首先是:runAndReset()这个方法,然后是setNextRunTime()这个方法,然后是reExecutePeriodic(outerTask)这个方法。

第一个方法runAndReset()貌似是执行我们的提交的任务的,我们看下代码:protected boolean runAndReset() {        if (state != NEW ||

!UNSAFE.compareAndSwapObject(this, runnerOffset,                                         null, Thread.currentThread()))            return false;        boolean ran = false;        int s = state;        try {

Callable c = callable;            if (c != null && s == NEW) {                try {

c.call(); // don't set result

ran = true;

} catch (Throwable ex) {

setException(ex);

}

}

} finally {            // runner must be non-null until state is settled to

// prevent concurrent calls to run()

runner = null;            // state must be re-read after nulling runner to prevent

// leaked interrupts

s = state;            if (s >= INTERRUPTING)

handlePossibleCancellationInterrupt(s);

}        return ran && s == NEW;

}

关键的地方是c.call()这一句,这个c就是我们提交的任务。

第二个方法setNextRunTime()的意思是设置下次执行的时间,下面是他的代码细节:private void setNextRunTime() {            long p = period;            if (p > 0)

time += p;            else

time = triggerTime(-p);

}

我们只需要看p>0这个分支就可以了,其实这是两种策略。我们的示例对应了第一个分支的策略,所以很显然,time这个变量会被加p,而p则是我们设定好的period。下面我们找一下这个time是在哪里初始化的,回忆一下scheduleAtFixedRate这个方法的内,我们说我们的任务被包装了两次,而time就是在这里被初始化的:/**

* Returns the trigger time of a delayed action.

*/

private long triggerTime(long delay, TimeUnit unit) {        return triggerTime(unit.toNanos((delay 

}    /**

* Returns the trigger time of a delayed action.

*/

long triggerTime(long delay) {        return now() +

((delay > 1)) ? delay : overflowFree(delay));

}

无论如何,我们知道一个任务会被运行完一次之后再次设置时间,然后线程池会获取任务来执行,而任务队列是一个延时阻塞队列,所以也就造成了周期性运行的假象。可以看下下面获取任务的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

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

}

}

可以看到,如果delay小于等于0,那么就是说需要被立即调度,否则延时delay这样一段时间。也就是延时消费。

结论就是,一个任务会被重复添加到一个延时任务队列,所以同一时间任务队列中会有多个任务待调度,线程池会首先获取优先级高的任务执行。如果我们的任务运行时间大于设置的调度时间,那么效果就是任务运行多长时间,调度时间就会变为多久,因为添加到任务队列的任务的延时时间每次都是负数,所以会被立刻执行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值