欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

java定时任务_「原创」Java并发编程系列35 | ScheduledThreadPoolExecutor定时器

程序员文章站 2022-05-23 10:13:54
...

★★★建议星标我们★★★

java定时任务_「原创」Java并发编程系列35 | ScheduledThreadPoolExecutor定时器java定时任务_「原创」Java并发编程系列35 | ScheduledThreadPoolExecutor定时器

2020年Java原创面试题库连载中

【000期】Java最全面试题库思维导图

【001期】JavaSE面试题(一):面向对象

【002期】JavaSE面试题(二):基本数据类型与访问修饰符

【003期】JavaSE面试题(三):JavaSE语法(1)

【004期】JavaSE面试题(四):JavaSE语法(3)

【005期】JavaSE面试题(五):String类

【006期】JavaSE面试题(六):泛型

【007期】JavaSE面试题(七):异常

【008期】JavaSE面试题(八):集合之List

【009期】JavaSE面试题(九):集合之Set

【010期】JavaSE面试题(十):集合之Map

【011期】JavaSE面试题(十一):多线程(1)

【012期】JavaSE面试题(十二):多线程(2)

【013期】JavaSE面试题(十三):多线程(3)

【014期】JavaSE面试题(十四):基本IO流

【015期】JavaSE面试题(十五):网络IO流

【016期】JavaSE面试题(十六):反射

【017期】JavaSE面试题(十七):JVM之内存模型

【018期】JavaSE面试题(十八):JVM之垃圾回收

【020期】JavaSE系列面试题汇总(共18篇)

【019期】JavaWeb面试题(一):JDBC

【021期】JavaWeb面试题(二):HTTP协议

【022期】JavaWeb面试题(三):Cookie和Session

【023期】JavaWeb面试题(四):JSP

【024期】JavaWeb面试题(五):Filter和Listener

【025期】Java工具面试题(一):版本控制工具

【026期】Java工具面试题(二):项目管理工具

【027期】Java设计模式面试题

【028期】JavaWeb系列面试题汇总(共10篇)

【029期】JavaEE面试题(一)Web应用服务器

【030期】JavaEE面试题(二)SpringMVC

【031期】JavaEE面试题(三)Spring(1)

【032期】JavaEE面试题(四)Spring(2)

【033期】JaveEE面试题(五)MyBatis

【034期】JavaEE面试题(六)Hibernate

【035期】JavaEE面试题(七)SpringBoot(1)

更多内容,点击上面蓝字查看

java定时任务_「原创」Java并发编程系列35 | ScheduledThreadPoolExecutor定时器java定时任务_「原创」Java并发编程系列35 | ScheduledThreadPoolExecutor定时器

上一篇讲解了线程池的原理,这篇就在线程池基础上介绍基于线程池实现的定时器ScheduledThreadPoolExecutor:

  1. ScheduledThreadPoolExecutor的用法

  2. ScheduledThreadPoolExecutor源码分析

  3. ScheduledThreadPoolExecutor执行过程分析

1. 介绍

ScheduledThreadPoolExecutor 可以用来在给定延时后执行异步任务或者周期性执行任务,也就是我们说的定时器。ScheduledThreadPoolExecutor基于线程池,通过多线程实现延时和周期执行。

1.1 用法Demo

如下代码使用ScheduledThreadPoolExecutor实现:10ms后打印第一次,之后每隔30ms打印一次。

public class TimerDemo {
public static void main(String[] args) {
// 1. 创建线程池定时器
ScheduledExecutorService timer = Executors.newScheduledThreadPool(3);
// 2. 提交定时任务:10ms后打印第一次,之后每隔30ms打印一次
timer.scheduleAtFixedRate(new Runnable {
@Override
public void run {
System.out.println(1);
}
}, 10, 30, TimeUnit.MILLISECONDS);
}
}

1.2 四种定时器用法

  1. 第一种schedule(Runnable command, long delay, TimeUnit unit);达到给定的延时时间后,执行任务,Runnable不能返回结果;

  2. 第二种schedule(Callable

  3. 第三种scheduleAtFixedRate; 固定周期执行任务,每次执行的开始时间之间的间隔是固定的,最开始就能够确定之后每次执行的时间;

  4. 第四种scheduleWithFixedDelay; 固定延时周期执行任务,上一次执行结束到下一次执行开始的间隔时间是固定的,由于每次执行任务花费时间不一定相同,所以只有在上次执行结束之后才能确定下次执行开始的时间。

 /**
* 达到给定的延时时间后,执行任务
* @param command Runnable接口的任务,ScheduledFuture.get获取结果为
*/
public ScheduledFuture> schedule(Runnable command, long delay, TimeUnit unit);

/**
* 到给定的延时时间后,执行任务。
* @param callable 实现Callable接口的任务,ScheduledFuture.get可获取任务结果
*/
public ScheduledFuture schedule(Callable callable, long delay, TimeUnit unit);
/**
* 固定周期执行任务
* @param initialDelay 第一次执行的延迟时间
* @param period 周期
*/
public ScheduledFuture> scheduleAtFixedRate(Runnable command,
long initialDelay, long period, TimeUnit unit);
/**
* 固定延时周期执行任务
* @param initialDelay 第一次执行的延迟时间
* @param delay 上一次执行结束到下一次执行开始的间隔时间
*/
public ScheduledFuture> scheduleWithFixedDelay(Runnable command,
long initialDelay, long delay, TimeUnit unit);

2. 类结构

2.1 继承结构

java定时任务_「原创」Java并发编程系列35 | ScheduledThreadPoolExecutor定时器
  1. ScheduledThreadPoolExecutor 继承了ThreadPoolExecutor,是一种特殊的线程池,拥有 execute和 submit提交异步任务功能。

  2. ScheduledThreadPoolExecutor 类实现了ScheduledExecutorService,该接口定义了延时执行任务和周期执行任务的功能;

  3. ScheduledThreadPoolExecutor 有两个重要的内部类:DelayedWorkQueue和ScheduledFutureTask。DelayedWorkQueue 实现了 BlockingQueue 接口,是一个阻塞队列;ScheduledFutureTask 继承了 FutureTask 类,是一个可以返回异步任务的结果的Runnable。

2.2 构造方法

通过构造方法可以看到,创建ScheduledThreadPoolExecutor其实就是创建一个线程池,corePoolSize可以指定,maximumPoolSize为Integer.MAX_VALUE,任务队列为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);
}

2.3 ScheduledFutureTask

ScheduledThreadPoolExecutor 提交的任务时,将任务封装成 ScheduledFutureTask,当执行任务时通过ScheduledFutureTask的run方法调用任务的run方法。

ScheduledFutureTask的compareTo方法用于延迟队列排序,按照执行时间的升序来排列,执行时间距离当前时间越近的任务在队列的前面。

private class ScheduledFutureTask
extends FutureTask implements RunnableScheduledFuture {
private long time;// 下次执行时间
private final long period;// 周期
/**
* 比较方法,到下次执行时间短的任务优先
*/
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;
}
/**
* ScheduledThreadPoolExecutor提交的任务被封装成ScheduledFutureTask,所以任务执行要通过这个run方法
*/
public void run {
boolean periodic = isPeriodic;// 是否是周期执行
// 当前线程池运行状态下如果不可以执行任务,取消该任务
if (!canRunInCurrentRunState(periodic))
cancel(false);
// 如果不是周期性任务,直接调用FutureTask中的run方法执行
else if (!periodic)
ScheduledFutureTask.super.run;
// 如果是周期性任务,调用FutureTask中的runAndReset方法执行
else if (ScheduledFutureTask.super.runAndReset) {
setNextRunTime;// 计算下次执行该任务的时间
reExecutePeriodic(outerTask);// 将新任务再次放入线程池等待被执行任务
}
}
}

2.4 DelayedWorkQueue

DelayedWorkQueue 是一个基于小顶堆的数据结构,类似于 DelayQueue 和 PriorityQueue。DelayedWorkQueue 按照执行时间的升序来排列,执行时间距离当前时间越近的任务在队列的前面。之前已经详细介绍过 DelayQueue 和 PriorityQueue了,这里就不在重复了。

  1. DelayedWorkQueue是存储ScheduledFutureTask阻塞队列。

  2. 插入元素时,会根据延期时间对元素排序,队头的元素是最先到期的;

  3. 取出元素时,只有在队头元素到期时才能够从队列中取元素。如果队头元素还有t时间到期,则将取出元素线程阻塞t时间,t时间到后再次尝试取出队头元素。

static class DelayedWorkQueue extends AbstractQueue
implements BlockingQueue {
// 队列初始容量
private static final int INITIAL_CAPACITY = 16;
// 根据初始容量创建RunnableScheduledFuture类型的数组
private RunnableScheduledFuture> queue =
new RunnableScheduledFuture>[INITIAL_CAPACITY];
private final ReentrantLock lock = new ReentrantLock;
private int size = 0;
//leader线程
private Thread leader = ;
//当较新的任务在队列的头部可用时,或者新线程可能需要成为leader,则通过该条件发出信号
private final Condition available = lock.newCondition;
/**
* 根据延期时间对元素排序,队头的元素是最先到期的
*/
public boolean offer(Runnable x) {}
/**
* 只有在队头元素到期时才能够从队列中取元素。
* 如果队头元素还有t时间到期,则将取出元素线程阻塞t时间,t时间到后再次尝试取出队头元素。
*/
public RunnableScheduledFuture> take throws InterruptedException {}
// ......
}

3. 执行过程

以上文的小示例为例,通过源码来分析ScheduledThreadPoolExecutor的执行过程。

示例代码:

public class TimerDemo {
public static void main(String[] args) {
// 1. 创建线程池定时器
ScheduledExecutorService timer = Executors.newScheduledThreadPool(3);
// 2. 提交定时任务:10ms后打印第一次,之后每隔30ms打印一次
timer.scheduleAtFixedRate(new Runnable {
@Override
public void run {
System.out.println(1);
}
}, 10, 30, TimeUnit.MILLISECONDS);
}
}

3.1 创建线程池定时器

创建线程池定时器就是创建一个线程池:

/**
* Executors.newScheduledThreadPool(3);
*/
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}

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

public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory, defaultHandler);
}

3.2 添加任务

  1. Runable任务封装成ScheduledFutureTask;

  2. 任务加入延时队列,同时在队列中按照执行的时间顺序排序,最先执行的任务在队头;

  3. 确保线程池中有活动线程,如果没有就启动一个。

public ScheduledFuture> scheduleAtFixedRate(Runnable command,
long initialDelay, long period, TimeUnit unit) {
// 一些检查
if (command == || unit == )
throw new PointerException;
if (period <= 0)
throw new IllegalArgumentException;

// 将command封装成ScheduledFutureTask,包括下次执行时间和周期
ScheduledFutureTask sft = new ScheduledFutureTask(command,
, triggerTime(initialDelay, unit), unit.toNanos(period));
RunnableScheduledFuture t = decorateTask(command, sft);
sft.outerTask = t;// 设置下次执行的任务
// 将ScheduledFutureTask类型的任务放入线程池延时执行,下文详细介绍
delayedExecute(t);
return t;
}
/**
* 将ScheduledFutureTask类型的任务放入线程池延时执行
*/
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;// 确保线程池中有线程执行
}
}
/**
* 确保线程池中有线程执行
* 只有两种情况会启动线程:
* 1. 当前线程数小于corePoolSize,以corePoolSize为界限启动一个线程
* 2. 线程池参数corePoolSize=0且此时线程池中没有线程,以maximumPoolSize为界限启动一个线程
*/
void ensurePrestart {
int wc = workerCountOf(ctl.get);
// 线程数小于corePoolSize,以corePoolSize为界限启动一个线程
if (wc < corePoolSize)
addWorker(, true);
/*
* 线程数大于等于corePoolSize且线程数==0,其实就是线程池参数corePoolSize=0且此时线程池中没有线程
* 以maximumPoolSize为界限启动一个线程
*/
else if (wc == 0)
addWorker(, false);
}

3.3 执行任务

  1. 线程池中的活动线程会循环到任务队列中取任务,当队头任务还没到期时,线程阻塞至队头任务到期时间,然后再取任务;

  2. 取出任务后执行,因为任务是ScheduledFutureTask类型(添加任务时封装的),执行ScheduledFutureTask.run;

  3. ScheduledFutureTask.run执行当前任务,设置下次执行时间并将任务放入线程池;

  4. 线程池中的活动线程会循环到任务队列中取任务,...循环...

/**
* ScheduledThreadPoolExecutor提交的任务被封装成ScheduledFutureTask,所以任务执行要通过这个run方法
*/
public void run {
boolean periodic = isPeriodic;// 是否是周期执行
// 当前线程池运行状态下如果不可以执行任务,取消该任务
if (!canRunInCurrentRunState(periodic))
cancel(false);
// 如果不是周期性任务,直接调用FutureTask中的run方法执行
else if (!periodic)
ScheduledFutureTask.super.run;
// 如果是周期性任务,调用FutureTask中的runAndReset方法执行
else if (ScheduledFutureTask.super.runAndReset) {
setNextRunTime;// 计算下次执行该任务的时间
reExecutePeriodic(outerTask);// 将新任务再次放入线程池等待被执行任务
}
}

/**
* 计算下次执行该任务的时间
*/
private void setNextRunTime {
long p = period;
if (p > 0)
time += p;
else
time = triggerTime(-p);
}

/**
* 将新任务再次放入线程池等待被执行任务
*/
void reExecutePeriodic(RunnableScheduledFuture> task) {
if (canRunInCurrentRunState(true)) {
super.getQueue.add(task);// 加入延时队列
// 删除不符合条件任务
if (!canRunInCurrentRunState(true) && remove(task))
task.cancel(false);
else
ensurePrestart;// 确保线程池中有线程执行
}
}

3.3 执行过程总结

  1. Runable任务封装成ScheduledFutureTask;

  2. 任务加入延时队列,同时在队列中按照执行的时间顺序排序,最先执行的任务在队头;

  3. 确保线程池中有活动线程,如果没有就启动一个;

  4. 线程池中的活动线程会循环到任务队列中取任务,当队头任务还没到期时,线程阻塞至队头任务到期时间,然后再取任务;

  5. 取出任务后执行,因为任务是ScheduledFutureTask类型(添加任务时封装的),执行ScheduledFutureTask.run;

  6. ScheduledFutureTask.run执行当前任务,设置下次执行时间并将任务放入线程池;

  7. 线程池中的活动线程会循环到任务队列中取任务,...循环...

java定时任务_「原创」Java并发编程系列35 | ScheduledThreadPoolExecutor定时器

4. scheduleAtFixedRate VS scheduleWithFixedDelay

  • scheduleAtFixedRate; 固定周期执行任务,每次执行的开始时间之间的间隔是固定的,最开始就能够确定之后每次执行的时间;

  • scheduleWithFixedDelay; 固定延时周期执行任务,上一次执行结束到下一次执行开始的间隔时间是固定的,由于每次执行任务花费时间不一定相同,所以只有在上次执行结束之后才能确定下次执行开始的时间。

从源码角度理解scheduleAtFixedRate和scheduleWithFixedDelay的不同,由两个细节决定:

细节一:构造ScheduledFutureTask时,scheduleAtFixedRate传入period(>0),而scheduleWithFixedDelay传入-delay(<0)。

public ScheduledFuture> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit) {
if (command == || unit == )
throw new PointerException;
if (period <= 0)
throw new IllegalArgumentException;
ScheduledFutureTask sft =
new ScheduledFutureTask(command,
,
triggerTime(initialDelay, unit),
unit.toNanos(period));// z这里是period
RunnableScheduledFuture t = decorateTask(command, sft);
sft.outerTask = t;
delayedExecute(t);
return t;
}
public ScheduledFuture> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit) {
if (command == || unit == )
throw new PointerException;
if (delay <= 0)
throw new IllegalArgumentException;
ScheduledFutureTask sft =
new ScheduledFutureTask(command,
,
triggerTime(initialDelay, unit),
unit.toNanos(-delay));// 这里是-delay
RunnableScheduledFuture t = decorateTask(command, sft);
sft.outerTask = t;
delayedExecute(t);
return t;
}

细节二:执行完一次后设置下次执行时间时,

  • p>0,scheduleAtFixedRate,下次执行开始时间=上次开始执行时间+周期

  • period<=0,scheduleWithFixedDelay,下次执行开始时间=上次结束执行时间(now)+周期

 /**
* 计算下次执行该任务的时间
*/
private void setNextRunTime {
long p = period;
// p>0,scheduleAtFixedRate,下次执行开始时间=上次开始执行时间+周期
if (p > 0)
time += p;
// period<=0,scheduleWithFixedDelay,下次执行开始时间=上次结束执行时间+周期
else
time = triggerTime(-p);
}

long triggerTime(long delay) {
return now +
((delay < (Long.MAX_VALUE >> 1)) ? delay : overflowFree(delay));
}

5. 总结

  1. ScheduledThreadPoolExecutor 可以用来在给定延时后执行异步任务或者周期性执行任务,也就是我们说的定时器。ScheduledThreadPoolExecutor基于线程池,通过多线程实现延时和周期执行。

  2. ScheduledThreadPoolExecutor的四种用法:

  • schedule(Runnable command, long delay, TimeUnit unit);达到给定的延时时间后,执行任务,Runnable不能返回结果;

  • schedule(Callable

  • 第三种scheduleAtFixedRate; 固定周期执行任务,每次执行的开始时间之间的间隔是固定的,最开始就能够确定之后每次执行的时间;

  • 第四种scheduleWithFixedDelay; 固定延时周期执行任务,上一次执行结束到下一次执行开始的间隔时间是固定的,由于每次执行任务花费时间不一定相同,所以只有在上次执行结束之后才能确定下次执行开始的时间。

执行过程:

  1. Runable任务封装成ScheduledFutureTask;

  2. 任务加入延时队列,同时在队列中按照执行的时间顺序排序,最先执行的任务在队头;

  3. 确保线程池中有活动线程,如果没有就启动一个;

  4. 线程池中的活动线程会循环到任务队列中取任务,当队头任务还没到期时,线程阻塞至队头任务到期时间,然后再取任务;

  5. 取出任务后执行,因为任务是ScheduledFutureTask类型(添加任务时封装的),执行ScheduledFutureTask.run;

  6. ScheduledFutureTask.run执行当前任务,设置下次执行时间并将任务放入线程池;

  7. 线程池中的活动线程会循环到任务队列中取任务,...循环...

并发系列文章汇总

【原创】01|开篇获奖感言

【原创】02|并发编程三大核心问题

【原创】03|重排序-可见性和有序性问题根源

【原创】04|Java 内存模型详解

【原创】05|深入理解 volatile

【原创】06|你不知道的 final

【原创】07|synchronized 原理

【原创】08|synchronized 锁优化

【原创】09|基础干货

【原创】10|线程状态

【原创】11|线程调度

【原创】12|揭秘 CAS

【原创】13|LockSupport

【原创】14|AQS 源码分析

【原创】15|重入锁 ReentrantLock

【原创】16|公平锁与非公平锁

【原创】17|读写锁八讲(上)

【原创】18|读写锁八讲(下)

【原创】19|JDK8新增锁StampedLock

【原创】20|StampedLock源码解析

【原创】21|Condition-Lock的等待通知

【原创】22|倒计时器CountDownLatch

【原创】22|倒计时器CountDownLatch

【原创】23|循环屏障CyclicBarrier

【原创】24|信号量Semaphore

【原创】25|交换器Exchangere

【原创】26|ConcurrentHashMap(上)

【原创】27|ConcurrentHashMap(下)

【原创】28|Copy-On-Write容器

【原创】29|ConcurrentLinkedQueue

【原创】30 | ThreadLocal

【原创】31 | 阻塞队列(上)

【原创】32 | 阻塞队列(下)

【原创】33 | 深入理解线程池(上)

【原创】34 | 深入理解线程池(下)

java定时任务_「原创」Java并发编程系列35 | ScheduledThreadPoolExecutor定时器

之前,给大家发过三份Java面试宝典,这次新增了一份,目前总共是四份面试宝典,相信在跳槽前一个月按照面试宝典准备准备,基本没大问题。

《java面试宝典5.0》(初中级)

《350道Java面试题:整理自100+公司》(中高级)

《资深java面试宝典-视频版》(资深)

《Java[BAT]面试必备》(资深)

分别适用于初中级,中高级资深级工程师的面试复习。

内容包含java基础、javaweb、mysql性能优化、JVM、锁、百万并发、消息队列,高性能缓存、反射、Spring全家桶原理、微服务、Zookeeper、数据结构、限流熔断降级等等。

java定时任务_「原创」Java并发编程系列35 | ScheduledThreadPoolExecutor定时器

看到这里,证明有所收获

相关标签: java定时任务