Java多线程之定时任务 以及 SpringBoot多线程实现定时任务、以及分享动态实现定时任务

  • 1. 基于单线程的定时器——简单介绍 Timer 中的 schedule 与 scheduleAtFixedRate
  • 1.1 前言
  • 1.2 先说 schedule
  • 附代码:
  • 1.3 schedule 与 scheduleAtFixedRate 的区别
  • 小结
  • 附代码
  • 2. 基于多线程的定时器——ScheduledExecutorService
  • 2.1 executorService.schedule
  • 2.2 scheduleAtFixedRate 与 scheduleWithFixedDelay 区别
  • 2.2.1 executorService.scheduleAtFixedRate
  • 2.2.2 executorService.scheduleWithFixedDelay
  • 2.2.3 小节
  • 3. 基于多线程的定时器(SpringBoot)
  • 3.1 提前了解 cron表达式和@Async异步
  • 3.2 @EnableScheduling 与 @Scheduled
  • 3.3 @Scheduled 定时任务例子
  • 3.3.1 关于 cron 表达式的
  • 3.3.1.1 cron 例1-->单个任务
  • (1)简单分析@Schedule默认定时任务的线程
  • 3.3.1.2 cron 例2-->多个任务(非异步)
  • 3.3.1.3 cron 例3-->多个任务(异步@Async)
  • 3.3.1.4 cron 例4-->多个任务(异步@Async——自定义线程池)
  • 3.3.1.5 从配置文件读取 cron表达式 以及停掉 cron任务
  • (1)cron 例5-->用符号 “ - ”控制停止定时任务
  • (2)cron 例6-->手动控制定时任务的开启和停止
  • a. 分析 @EnableScheduling
  • a. 配置文件设置定时任务的开关
  • 3.3.2 其他类型参数——简单说
  • 4. springboot动态定时任务的实现
  • 5. 附项目工程代码


1. 基于单线程的定时器——简单介绍 Timer 中的 schedule 与 scheduleAtFixedRate

1.1 前言

  • Timer,一般是用来做延时的任务或者循环定时执行的任务。
    使用 Timer 的时候,必须要有一个 TimerTask 去执行任务,这是一个实现了Runnable接口的线程,run 方法里面就是我们自己定义的线程需要做的任务。

1.2 先说 schedule

  • 我们简单说4种情况,其实也就两个重载方法:
  • timer.schedule(timerTask,0);,没用延迟,立即执行task,且只执行一次,线程还在。 代码和效果直接看图
  • timer.schedule(timerTask,5000);,延迟5秒执行task,且只执行一次,线程还在。代码和效果直接看图
  • timer.schedule(timerTask,0,60000);,0表示没用延迟,立即执行,然后60000表示每隔1分钟执行一次task。代码和效果直接看图
  • timer.schedule(timerTask,5000,60000);,延迟5秒后执行task,然后每隔1分钟执行一次task,代码和效果直接看图:

附代码:

package com.liu.susu.thread.task.timer;

import java.time.LocalDateTime;
import java.util.Timer;
import java.util.TimerTask;

/**
 * @FileName TimeTaskTest1
 * @Description
 * @Author susu
 * @date 2022-03-07
 **/
public class TimerTaskTest1 {
    public static void main(String[] args) {
        Timer timer = new Timer();
        TimerTask timerTask = new TimerTask() {//TimerTask 是实现 Runnable 接口的一个线程
            @Override
            public void run() {
                System.out.println("task-->hello world!-->"+LocalDateTime.now());
            }
        };
        System.out.println("begin-->"+LocalDateTime.now());

//        timer.schedule(timerTask,0);//没用延迟,立即执行task,且只执行一次,线程还在
//        timer.schedule(timerTask,5000);//延迟5秒执行task,,且只执行一次,线程还在
//        timer.schedule(timerTask,0,60000);//0表示没用延迟,立即执行,然后60000表示每隔1分钟执行一次task
        timer.schedule(timerTask,5000,60000);//延迟5秒后执行task,然后每隔1分钟执行一次task
    }
}

1.3 schedule 与 scheduleAtFixedRate 的区别

  • 关于上述介绍的 schedule 的4种调用情况,scheduleAtFixedRate 同样也有,最终效果也是一致的,我们这里就不介绍了,说说它两不一样的地方。什么时候不一样呢?当给他们设置指定开始任务的时间时,它们的区别就有了。
    主要是 public void scheduleAtFixedRate(TimerTask task, Date firstTime, long period) 方法 与 public void schedule(TimerTask task, Date firstTime, long period) 方法的区别。
  • 我们先看官网怎么说的:
  • 如果不是很明白看我们下面的效果图:

小结

  • void schedule(TimerTask task, Date firstTime,long period)方法的任务的计划执行时间是从第一次实际执行任务开始计算的。
    如果执行任务的时间没有被延时,那么下一次任务的执行时间参考上一次任务的“开始”时的时间来计算
  • scheduleAtFixedRate 会计算首次调用时间,在执行时会先计算出错过的时间内 task 应执行的次数,再去按设定频率去执行。
  • 具体原理,还需要大家看源码分析!

附代码

package com.liu.susu.thread.task.timer;

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

/**
 * @FileName TimerTaskTest2
 * @Description
 * @Author susu
 * @date 2022-03-07
 **/
public class TimerTaskTest2 {
    public static void main(String[] args) {
        Timer timer = new Timer();
        TimerTask timerTask = new TimerTask() {
            @Override
            public void run() {
                System.out.println("task-->"+ LocalDateTime.now());
            }
        };
        Date firstDateTime = getFirstDateTime();
        System.out.println("firstDateTime-->"+firstDateTime+"<=====>now-->"+LocalDateTime.now()+"\n");
        timer.schedule(timerTask,firstDateTime,10000);
        timer.scheduleAtFixedRate(timerTask,firstDateTime,10000);
    }
    public static Date getFirstDateTime(){
        String dateStr = "2022-03-07 16:30:34";
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        LocalDateTime localDateTime = LocalDateTime.parse(dateStr, dateTimeFormatter);
        Date date = Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
        return date;
    }

}

2. 基于多线程的定时器——ScheduledExecutorService

  • ScheduledExecutorService 是一个接口,实现类是ScheduledThreadPoolExecutor,是通过 Executors 自动创建线程池的一种方式(newScheduledThreadPool 方式)
  • 关于线程池的创建可以看:
    详解Java多线程之线程池.
  • 关于 ScheduledThreadPoolExecutor,我们下面就简单说3个方法,完整代码如下:
package com.liu.susu.thread.task.pool;

import java.time.LocalDateTime;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * @FileName ScheduledThreadPoolTest
 * @Description
 * @Author susu
 * @date 2022-03-07
 **/
public class ScheduledThreadPoolTest {
    public static void main(String[] args) {
        ScheduledExecutorService executorService = Executors.newScheduledThreadPool(5);
        
        System.out.println("当前时间是-->"+LocalDateTime.now());
        
        //1.schedule--->只延迟执行,且只执行一次(不循环)
        executorService.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()
                        + "-->" + LocalDateTime.now());
            }
        },5,TimeUnit.SECONDS);//推迟5秒执行

        //2.scheduleAtFixedRate 推迟执行(initialDelay=0,不推迟),然后周期性循环执行task
        executorService.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()
                        + "-->" + LocalDateTime.now());
                try {
                    Thread.sleep(5000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        },5,10,TimeUnit.SECONDS);//推迟5秒执行,然后每10秒执行一次

        //3.scheduleWithFixedDelay 推迟执行(initialDelay=0,不推迟),然后周期性循环执行task
        executorService.scheduleWithFixedDelay(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()
                        + "-->" + LocalDateTime.now());
                try {
                    Thread.sleep(5000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        },5,10,TimeUnit.SECONDS);

    }
}

2.1 executorService.schedule

  • 创建并执行在给定延迟后启用的单次操作。
  • 效果图:
  • 这个没啥可说的了,上面的内容看了的,这个很好理解的,我们主要说一下 scheduleAtFixedRate 与 scheduleWithFixedDelay 的区别。

2.2 scheduleAtFixedRate 与 scheduleWithFixedDelay 区别

  • 来我们,先看官方怎么说的
  • 这是什么意思呢,我们看看下面的效果就非常明白了

2.2.1 executorService.scheduleAtFixedRate

  • 效果图:
  • 上面大概明白怎么走的了吧,区别到底在哪里,先别急,看完下面的效果就明白了!

2.2.2 executorService.scheduleWithFixedDelay

  • 效果图
  • 现在明白二者的区别了吧

2.2.3 小节

  • scheduleAtFixedRate:是以固定频率来执行线程任务,固定频率的含义就是可能设定的固定间隔时间不足以完成线程任务,但是它不管,达到设定的延迟时间了就要开始执行下一次任务了。
  • scheduleWithFixedDelay:不管线程任务的执行时间的长短,每次都要把任务执行完成后再延迟固定时间(设置的间隔时间)后再执行下一次的任务。

3. 基于多线程的定时器(SpringBoot)

3.1 提前了解 cron表达式和@Async异步

3.2 @EnableScheduling 与 @Scheduled

  • 关于 @EnableScheduling :
    在SpringBoot项目中,在启动类或者定时任务类上添加 @EnableScheduling 注解来开启对定时任务的支持。
  • 关于 @Scheduled :
    如果在定时任务类中的方法上添加 @Scheduled 注解,则该方法是声明需要执行的定时任务。
  • @Scheduled 注解有几种类型参数,先截图下来,我们下面会简单根据例子介绍

3.3 @Scheduled 定时任务例子

  • 首先启动类上添加注解 @EnableScheduling ,下面不再提醒了。

3.3.1 关于 cron 表达式的

  • cron 表达式不是很清楚的,看我们上面提供的链接,此处不做解释了。
3.3.1.1 cron 例1–>单个任务
  • 用注解 @Scheduled(cron = "0/5 * * * * ?") (每5秒执行一次),代码很简单,直接截图:
  • 启动项目后,后台自动按cron表达式配置的执行任务,直接看运行效果:
(1)简单分析@Schedule默认定时任务的线程
  • 观察上面的执行结果,不难发现,Spring 的 Schedule 定时任务默认是单线程的。而且是默认所有定时任务都是在一个线程中执行(下面我们有多任务的例子)!
  • 这样的话,我们看到的只是代码层次的定时任务,而实际执行得过程是一个任务的开启需要等上一个任务的结束才行。尤其是当系统中有特别耗时的定时任务执行频繁的定时任务,执行频繁的任务需要等耗时的任务执行完才能执行,你说这算什么定时任务!
  • 会造成严重得后果就是如果我们有多个定时任务,一个卡死,其他全挂,嗯,就这样。
  • 问题分析出来了,解决是不是就有了,对,异步多线程!
3.3.1.2 cron 例2–>多个任务(非异步)
  • 用异步前,我们不妨来写两个定时任务看看效果,先看两个耗时都短的任务:

    很明显如果耗时短还不好观察,任务1是按定时5秒执行一次,任务2也按定时2秒执行一次,没看出来,但是能看出来是单线程的执行两个任务!。
  • 再来看一个耗时长,一个频繁执行的定时任务,我们让任务1睡一下就行了。

    这样对比效果就很明显了吧,这明显不是我们开发中想要的实现方式,所以考虑异步,请往下…
3.3.1.3 cron 例3–>多个任务(异步@Async)
  • 把上面的例子改成异步试试,很简单啥也不用,直接异步方法上加注解 @Async ,启动类加 @EnableAsync 注解即可。
    再说一下,这两个注解不太了解的,看下面文章,此处不做解释了。
    SpringBoot 自定义线程池以及多线程间的异步调用(@Async、@EnableAsync).
  • 修改后的代码如下(在例子3.3.1.2上修改):
  • 效果如图:

    可以看到在耗时任务1执行的过程中并不影响任务2定时任务的执行,也不影响任务1自己的定时任务,什么意思,意思就是你可以简单理解为同我们上述2.2.1中介绍的 executorService.scheduleAtFixedRate,即以固定频率来执行线程任务,就是在我们任务1中,设定的固定间隔时间5秒,而5秒不足以完成线程任务(需要10秒完成),但是它不管,达到设定的间隔时间5秒后就要开始执行下一次的任务1了(即:开启新的线程来执行)。
3.3.1.4 cron 例4–>多个任务(异步@Async——自定义线程池)
  • 3.3.1.3 中,虽然实现了异步,但是 @Async 也存在一个问题,就是默认线程池的问题,这个在这里不做介绍,上面链接的文章里已经说的很清楚,不明白的先了解一下 @Async 。
  • 所以,我们用自定义线程池的方式实现异步定时任务
  • 代码如下(文字代码上面链接介绍异步的文章里都有,这里截图一下看看,明白逻辑就行了):

  • 效果如下:
3.3.1.5 从配置文件读取 cron表达式 以及停掉 cron任务
  • 用配置文件的形式,主要方便我们以后更改任务的执行时间等。
  • 代码如图:
(1)cron 例5–>用符号 “ - ”控制停止定时任务

注意.properties 与.yml的区别写法

  • ① application.properties 中的 cron 表达式不能用双引号引起,而.yml中可以用双引号引起;
  • ② 在配置文件里配置让线程任务停掉,不是简单的注释掉,而是用减号符号 “-”,需注意的是两中配置文件写法不一样,yml中必须要加双引号(因为 - 在yml中是一个特殊的字符),application.properties 必须不能用双引号,这点需要注意。
(2)cron 例6–>手动控制定时任务的开启和停止
a. 分析 @EnableScheduling
  • 配置之前,我们先来分析一下源码,为什么启动类上加上 @EnableScheduling 注解就开启了对定时任务的支持了呢?好奇是吧,好奇就点呗,点进去看看…


    你如果感兴趣的话,可以继续往后点,自己欣赏源码也是打发时间的一种方式,哈哈哈哈!
    快烦了吧,说这么多什么意思呢?意思就是你可以不用这个注解 @EnableScheduling 让定时任务跑起来。怎么跑,先听解释:
  • 其实,任务方法上用的 @Scheduled 注解,是被一个叫做 ScheduledAnnotationBeanPostProcessor (上面最后一张截图里出现的)的类所拦截的,所以,根据源码的实现,我们也可以根据配置,决定是否创建这个 bean,如果有这个 bean ,定时任务正常执行,如果没有这个 bean,@Scheduled 就不会被拦截,那么定时任务肯定不会执行了了,嗯,就是这个道理。请往下看怎么实现:
a. 配置文件设置定时任务的开关
  • 首先,启动类上的注解 @EnableScheduling 注释掉。
  • 然后,配置文件添加开关,如图:
  • 然后,接下来我们说两种方式控制这个bean的创建
  • 方式一:接口 Condition 与注解 Conditional 结合

    两种方式说完,我们再测试效果
  • 方式二:直接用注解 @ConditionalOnProperty
  • 来,看一下效果吧,定时任务还是那两个任务


    ok了,就说这么多吧!

3.3.2 其他类型参数——简单说

  • 如果目录里的 1 和 2 都看明白了,关于 @Scheduled 注解的其他参数真的没什么可说的了,比葫芦画瓢的事了,我们就简单举个例子就行了

4. springboot动态定时任务的实现

  • 非动态,简单开关控制关于SpringBoot中定时任务@EnableScheduling的开关设置(针对指定的定时任务可控制)
  • 动态读取配置的定时任务SpringBoot定时任务的动态配置处理(动态获取数据库配置的定时任务).

5. 附项目工程代码

  • clone代码在下面的链接 由于省事,没有单独创建项目,代码就写在了之前的一个项目里,本次所有的代码(包括异步)都在如图所示的文件夹里:
  • Csdn上项目代码:Java多线程之定时任务 以及 SpringBoot多线程实现定时任务.
  • 码云上项目代码:https://gitee.com/liuersusu/springboot_jpa.
  • GitHub 上项目代码:https://github.com/liuersusu/springboot_jpa.