Java调度线程池ScheduleExecutorService
如果在一个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<Void> sft =
new ScheduledFutureTask<Void>(command,
null,
triggerTime(initialDelay, unit),
unit.toNanos(period));
RunnableScheduledFuture<Void> 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<V> 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 < 0) ? 0 : delay));
}
/**
* Returns the trigger time of a delayed action.
*/
long triggerTime(long delay) {
return now() +
((delay < (Long.MAX_VALUE >> 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这样一段时间。也就是延时消费。
结论就是,一个任务会被重复添加到一个延时任务队列,所以同一时间任务队列中会有多个任务待调度,线程池会首先获取优先级高的任务执行。如果我们的任务运行时间大于设置的调度时间,那么效果就是任务运行多长时间,调度时间就会变为多久,因为添加到任务队列的任务的延时时间每次都是负数,所以会被立刻执行。
作者:一字马胡
链接:https://www.jianshu.com/p/8c4c160ebdf7
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。
Java调度线程池ScheduleExecutorService的更多相关文章
- Java调度线程池ScheduleExecutorService(续)
链接 Java线程池详解(一) Java线程池详解(二) Java调度线程池ScheduleExecutorService 上面列出了最近写的关于java线程池ScheduleExecutorServ ...
- Java调度线程池ScheduledThreadPoolExecutor源码分析
最近新接手的项目里大量使用了ScheduledThreadPoolExecutor类去执行一些定时任务,之前一直没有机会研究这个类的源码,这次趁着机会好好研读一下. 该类主要还是基于ThreadPoo ...
- Java中线程池的学习
线程池的基本思想还是一种对象池的思想,开辟一块内存空间,里面存放了众多(未死亡)的线程,池中线程执行调度由池管理器来处理.当有线程任务时,从池中取一个,执行完成后线程对象归池,这样可以避免反复创建线程 ...
- ScheduledExecutorService调度线程池运行几次后停止某一个线程
开发中偶尔会碰到一些轮询需求,比如我碰到的和银行对接,在做完某一个业务后银行没有同步给到结果,这时候就需要查询返回结果,我们的需求是5分钟一次,查询3次,3次过后如果没有结果则T+1等银行的文件,对于 ...
- Java:线程池
Java:线程池 本笔记是根据bilibili上 尚硅谷 的课程 Java大厂面试题第二季 而做的笔记 获取多线程的方法: 实现 Runnable 接口 实现 Callable 接口 实例化 Thre ...
- 深入理解Java之线程池
原作者:海子 出处:http://www.cnblogs.com/dolphin0520/ 本文归作者海子和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则 ...
- java利用线程池处理集合
java利用线程池处理集合 2018年07月23日 17:21:19 衍夏成歌 阅读数:866 版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/s ...
- Java定时线程池停止超时任务
一.背景题主最近遇到一个问题,本来通过ScheduledExecutorService线程池定时调度一个任务.奈何不知道为啥跑了2个多月,其中一个任务Hang住了,原本定时的任务则出现了问题. 关于定 ...
- 深入理解Java之线程池(爱奇艺面试)
爱奇艺的面试官问 (1) 线程池是如何关闭的 (2) 如何确定线程池的数量 一.线程池销毁,停止线程池 ThreadPoolExecutor提供了两个方法,用于线程池的关闭,分别是shutdown() ...
随机推荐
- HTTP Modules versus ASP.NET MVC Action Filters
from:http://odetocode.com/blogs/scott/archive/2011/01/17/http-modules-versus-asp-net-mvc-action-filt ...
- iOS编程——Objective-C KVO/KVC机制[转]
这两天在看和这个相关的的内容,全部推翻重写一个版本,这是公司内做技术分享的文档总结,对结构.条理做了更清晰的调整.先找了段代码,理解下,网上看到最多的一段的关于KVC的代码 先上代码 1. 1 ...
- dssfsfsfs
一直都好使啊..好使 好使 MainActivity://overover package com.example.sockettest; import java.io.BufferedReader; ...
- WebApi与MVC Route 问题整理
1. 为WebAPI添加 Area后,完成了CustomControllerSelector的制定. 跟踪WebAPI底层,整理WebAPI源码后发现几个问题: 1. 使用Area的controlle ...
- ping包,支持ip录入
@echo off ::等待用户输入需要监控IP set /p ip=Input the IP required to monitor: echo executing...... :start ech ...
- Java Script 脚本的几种基本格式:
1. <script> document.Write("Hello wrrld!!!"); </script> 2. <scrip ...
- SQL Server Profiler (SQl跟踪器)的简单使用
一.工具介绍 在实际开发中,我们的数据库应用系统因为不可避免会存在有大量表,视图,索引,触发器,函数,存储过程,sql语句等,所以会出现一系列问题,有问题不可怕,找对工具很重要.接下来我就介绍一款性能 ...
- 「BJOI2012」连连看
题目链接 戳我 \(Solution\) 我们首先进行拆点操作,将每个点都拆成\(x\)和\(y\),将满足条件的两个点连起来就好了(记得要将\(x\)连\(y'\)的同时要将\(y\)联向\(x'\ ...
- stream.fail() eof() EOF
ifstream infile("student.dat"); while((c=infile.get())!=EOF){} //EOF的值是-1 但在 #include&l ...
- linux获取域名地址
dig live-195887137.cn-north-1.elb.amazonaws.com.cn +short