一、ScheduledThreadPoolExecutor介绍&应用
ScheduledThreadPoolExecutor是ThreadPoolExecutor的一个子类,在线程池的基础上实现了延迟执行任务以及周期性执行任务的功能。
Java最早提供的是Timer类执行定时任务,串行的,不靠谱,会影响到其他任务的执行。不采用第三方框架时,用ScheduledThreadPoolExecutor比较合适。
Quartz
xx-job
elastic-job
ScheduledThreadPoolExecutor就是在线程池的基础上实现的定时执行任务的功能。
ScheduledThreadPoolExecutor提供了比较常用的四种方法执行任务:不说callable
- execute: 跟普通线程池执行没区别
- schedule: 可以指定延迟时间,一次性执行任务。
- scheduleAtFixedRate: 可以让任务在固定的周期下执行。(任务的处理时间,不影响下次执行时间)。
如果任务的执行时间超过了设置的延迟时间,按照时间最长的计算 - scheduleWithFixedDelay:可以让任务在固定的周期下执行 。(任务的处理时间,会影响下次执行时间)
执行案例演示:
java
public static void main(String[] args) { ScheduledThreadPoolExecutor scheduledThreadPoolExecutor=new
ScheduledThreadPoolExecutor(10); scheduledThreadPoolExecutor.execute(()->{ System.out.println("hello world"); }); scheduledThreadPoolExecutor.schedule(()->{ System.out.println("hello world1"); },2000, TimeUnit.MILLISECONDS);// scheduledThreadPoolExecutor.scheduleAtFixedRate(()->{// System.out.println("hello world2:"+System.currentTimeMillis());// try {// Thread.sleep(1000);// } catch (InterruptedException e) {// e.printStackTrace();// }// },3000,2000,TimeUnit.MILLISECONDS); scheduledThreadPoolExecutor.scheduleWithFixedDelay(()->{ try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("hello world3"+System.currentTimeMillis()); },1000,2000,TimeUnit.MILLISECONDS); }
如果实际开发应用需要使用到定时任务,更推荐一些开源的框架,比如quartz,xx-job,elastic-job,因为corn表达式,对时间的控制更佳方便。
二、synchronousQueue底层结构
两个核心内容:
2.1 scheduledFutureTask
看到了核心内容,scheduledFutureTask间接的实现了Delay接口,让任务可以放到延迟队列中,并且基于二叉堆做排序,即将执行的时间越短,就往堆顶扔。
查看核心内容:
java
private class ScheduledFutureTask<V>
extends FutureTask<V> implements RunnableScheduledFuture<V> {
//计数器,每个任务进来时,都会有一个全局唯一的序号
//如果任务的执行时间一模一样,比对sequenceNumber
private final long sequenceNumber;
//任务执行的时间,单位是纳秒
private volatile long time;
//period ==0:标识一次性执行的任务
//period > 0:标识使用的是At
//period < 0:标识使用的是witch
private final long period;
//周期性实现任务时,引用具体任务,方便后面重新扔到阻塞队列
RunnableScheduledFuture<V> outerTask = this;
//有参构造,schedule时使用当前有参重载封装任务
ScheduledFutureTask(Runnable r, V result, long triggerTime,
long sequenceNumber) {
super(r, result);
this.time = triggerTime;
this.period = 0;
this.sequenceNumber = sequenceNumber;
}
//At,with时,使用当前有参重载封装任务
ScheduledFutureTask(Runnable r, V result, long triggerTime,
long period, long sequenceNumber) {
super(r, result);
this.time = triggerTime;
this.period = period;
this.sequenceNumber = sequenceNumber;
}
//有返回结果,不考虑
ScheduledFutureTask(Callable<V> callable, long triggerTime,
long sequenceNumber) {
super(callable);
this.time = triggerTime;
this.period = 0;
this.sequenceNumber = sequenceNumber;
}
//重写delay接口方法
public long getDelay(TimeUnit unit) {
return unit.convert(time - System.nanoTime(), NANOSECONDS);
}
//实现delayed接口重写方法,比较的方式,数据放到二叉堆内部
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;
}
//判断是否是周期执行
public boolean isPeriodic() {
return period != 0;
}
2.2 DelayedWorkQueue
已学过,本次忽略
三、schedule方法分析
封装任务-放延迟队列-创建线程准备执行
将传入的command任务和延迟执行的时间封装
java
//分析定时任务线程的schedule,延迟一段时间,执行一次command任务
public ScheduledFuture<?> schedule(Runnable command,
long delay,
TimeUnit unit) {
//分开判断
if (command == null || unit == null)
throw new NullPointerException();
// 封装任务,将普通的command封装为ScheduledFutureTask
//decorateTask 方法默认啥都没做,是线程池提供的扩展方法,可以在此位置修改任务需要执行的方式
RunnableScheduledFuture<Void> t = decorateTask(command,
new ScheduledFutureTask<Void>(command, null,
triggerTime(delay, unit),
sequencer.getAndIncrement()));
//延迟执行
delayedExecute(t);
return t;
}
private long triggerTime(long delay, TimeUnit unit) {
return triggerTime(unit.toNanos((delay < 0) ? 0 : delay));
}
//查看triggerTime方法重载,返回当前任务要执行的系统时间
long triggerTime(long delay) {
//判断延迟delay时间是否小于LONG.MAX_VALUE>>1,
//如果小于,正常计算执行的时间
//如果大于,可能出现超出long的取值范围问题,做额外处理
return System.nanoTime() +
((delay < (Long.MAX_VALUE >> 1)) ? delay : overflowFree(delay));
}
//将command任务封装为scheduleFutrueTask
ScheduledFutureTask(Runnable r, V result, long triggerTime,
long sequenceNumber) {
//任务要执行的系统时间
super(r, result);
this.time = triggerTime;
//任务是否是周期性执行
this.period = 0;
//基于atomic
this.sequenceNumber = sequenceNumber;
}
// 是线程池提供的扩展方法,可以在此位置修改任务需要执行的细节
protected <V> RunnableScheduledFuture<V> decorateTask(
Runnable runnable, RunnableScheduledFuture<V> task) {
return task;
}
//延迟执行
private void delayedExecute(RunnableScheduledFuture<?> task) {
//线程停止,是则拒绝执行
if (isShutdown())
reject(task);
else {
//线程池状态是RUNNING
//调用阻塞队列,将任务添加进去,将任务扔到延迟队列中(二叉堆)
//在添加任务到延迟队列的数组时,会记录当前任务所在的索引位置,方便取消任务时,从数组中去除
//heapIndex方便取消任务
super.getQueue().add(task);
//判断线程池是否不是running状态,如果不是running,就根据策略决定任务是否执行
//如果任务不需要执行了,调用remove方法,将任务从延迟队列移除,并且在if内部取消任务
if (!canRunInCurrentRunState(task) && remove(task))
task.cancel(false);
else
//线程池状态没毛病,任务是需要执行的。
ensurePrestart();
}
}
//如果任务添加到阻塞队列中,忽然线程池不是running状态,此时任务是否执行
//periodic-true:周期性执行的任务,false-代表一次性延迟的任务
boolean canRunInCurrentRunState(RunnableScheduledFuture<?> task) {
//判断当前线程池状态
if (!isShutdown())
return true;
if (isStopped())
return false;
return task.isPeriodic()
? continueExistingPeriodicTasksAfterShutdown
: (executeExistingDelayedTasksAfterShutdown
|| task.getDelay(NANOSECONDS) <= 0);
//默认情况下,如果任务扔到了延迟队列中,有两个策略
//如果任务是周期性执行的,默认为false
//如果任务是一次性执行的,默认为true
}
//准备执行任务
void ensurePrestart() {
//获取线程池中的工作线程个数
int wc = workerCountOf(ctl.get());
//如果工作线程个数,小于核心线程个数
if (wc < corePoolSize)
//创建核心线程
addWorker(null, true);
//如果工作线程池不小于核心线程,但是值为0,创建非核心线程执行任务
else if (wc == 0)
//创建非核心线程处理阻塞队列任务,而且只要阻塞队列没有任务了,当前线程立即消耗
addWorker(null, false);
}
查看任务放到延迟队列后,是如何被工作线程取出来执行的。
执行addWorker方法,会创建一个工作线程,工作线程在创建成功后,会执行start方法。在start方法执行后会调用worker的run方法,最终执行了runWorker方法,在runworker方法中会在阻塞队列的位置执行一直阻塞拿Ruable方法,拿到返回,就执行。
所以需要查看的就是延迟队列的take方法,套路和之前的delayQueue没有区别。
在拿到任务后,会执行任务,也就是执行任务的run方法
java
//执行任务
public void run() {
//判断线程池状态是不是running,如果不是running,且shutdown情况不允许执行,或者是stop状态
if (!canRunInCurrentRunState(this))
//取消任务
cancel(false);
else if (!isPeriodic())
//当前任务是一次性执行,执行run方法执行完没了
super.run();
else if (super.runAndReset()) {
//必然是周期执行
setNextRunTime();
reExecutePeriodic(outerTask);
}
}
四、scheduleAtFixedRate和scheduleWithFixedDelay分析
在执行方法的初期,封装任务时:
- At会将period设置为正数,代表固定周期执行
- With会将periods设置为负数,代表在执行任务完毕后,再计算下次执行的时间
java
//固定周期执行任务,如果任务的执行时间超过周期,任务执行完,立即执行下一次任务
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
//第一次执行时间
long initialDelay,
//周期执行时间
long period,
TimeUnit unit) {
if (command == null || unit == null)
throw new NullPointerException();
//健壮性判断
if (period <= 0L)
throw new IllegalArgumentException();
ScheduledFutureTask<Void> sft =
new ScheduledFutureTask<Void>(command,
null,
triggerTime(initialDelay, unit),
unit.toNanos(period),
sequencer.getAndIncrement());
//扩展
RunnableScheduledFuture<Void> t = decorateTask(command, sft);
//将任务设置给outerTask属性,方便后期重新扔到延迟队列
sft.outerTask = t;
//延迟队列执行
delayedExecute(t);
return t;
}
//固定周期执行任务,会在任务执行完毕后,再计算下次执行的时间
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
//第一次执行时间
long initialDelay,
//周期执行时间
long delay,
TimeUnit unit) {
if (command == null || unit == null)
throw new NullPointerException();
//健壮性判断
if (delay <= 0L)
throw new IllegalArgumentException();
ScheduledFutureTask<Void> sft =
new ScheduledFutureTask<Void>(command,
null,
triggerTime(initialDelay, unit),
-unit.toNanos(delay),
sequencer.getAndIncrement());
RunnableScheduledFuture<Void> t = decorateTask(command, sft);
sft.outerTask = t;
delayedExecute(t);
return t;
}
最终两个方法都会调用delayedExecute方法去将任务扔到阻塞队列,并尝试是否需要构建工作线程,从而执行任务。
工作线程会监听延迟队列,拿到任务后会调用任务的run方法。
java
//执行任务
public void run() {
//查看At和with可以确定任务是周期执行
//判断线程池状态是不是running,如果不是running,且shutdown情况不允许执行,或者是stop状态
if (!canRunInCurrentRunState(this))
//取消任务
cancel(false);
else if (!isPeriodic())
//当前任务是一次性执行,执行run方法执行完没了
super.run();
//到这,先执行任务
else if (super.runAndReset()) {
//设置下一次任务的运行时间
setNextRunTime();
reExecutePeriodic(outerTask);
}
}
//计算任务下次执行时间,time 是任务执行的时间,这里是time的上次执行时间
private void setNextRunTime() {
//拿到当前任务的period
long p = period;
//period>0:At
if (p > 0)
//At:直接拿上次执行的时间,添加上周期时间,来决定下次执行的时间
time += p;
else
//period<0,with: 任务执行完,拿当前系统时间+延迟时间
time = triggerTime(-p);
}
//重新将任务扔到延迟队列中
void reExecutePeriodic(RunnableScheduledFuture<?> task) {
//线程池状态的判断
if (canRunInCurrentRunState(task)) {
//将任务扔到了延迟队列中
super.getQueue().add(task);
//扔到延迟队列后,再次判断线程池状态,是否需要取消任务
if (canRunInCurrentRunState(task) || !remove(task)) {
//判断是否需要创建线程
ensurePrestart();
return;
}
}
task.cancel(false);
}
异步、同步、阻塞、非阻塞。
同步:做了同步操作后,被调用者不会主动通知我结果,我需要主动查看结果。
异步:ajax,做了异步操作后,被调用这会主动通知我结果是什么。
阻塞:阻塞操作就是调用功能后,不能离开。
非阻塞:调用功能后,可以离开做其他事情。
同步阻塞:烧水功能时,不能离开,要一直盯着,并且烧水功能完后不会主动通知我
同步非阻塞:烧水功能时,可以离开,但是烧水功能完成后不会通知我
异步阻塞:烧水时,我不能做其他事情,但水烧开后会主动通知我.(这个操作没有)
异步非阻塞:执行烧水时,我可以做其他事情。并且水烧开后他可以主动通知我
http请求-请求一个接口查询结果,同步阻塞
Callable - 调用一个线程执行任务,有返回结果,我需要通过isDone查询或者get方法查询结果。这种就是同步非阻塞。
compatableFutrue-异步非阻塞