Java定时任务的几种实现方式

Java定时任务实现方式

定时任务底层的算法基础

  • 小顶堆:每一次任务的调度,都需要重新进行剩余任务的重新排序,效率性能较低
  • 时间轮算法:链表或者数组实现针对具体到某一天、某一小时、某一分、某一秒的具体定时任务不适合;round时间轮只是缓解了上述的链表或数组实现情况;**分层时间轮(cron表达式底层实现逻辑)**通过设置不同的时间轮,分层抽取调度,大大加快了任务调度的效率。
    在这里插入图片描述

常见的定时任务实现方式

  • JDK自带的Timer以及ScheduledExecutorService
  • Quartz:Java作业调度框架
  • Spring3.0自带的SpringTask(轻量级的Quartz)

Timer类

Timer类实现定时任务主要是通过其内部的schedule方法实现,而schedule方法参数一般存在三个:

  • TimeTask类型的对象
  • delay(long类型,表示延时多久开始执行)
  • period(long类型,表示定时任务执行的周期)

对于TimeTask类而言,其本身是一个抽象类,实现了Runnable接口,内部存在一个run方法,完成的即是定时任务的具体工作

//自定义一个Task任务类
public class Task extends TimerTask {
    @Override
    public void run() {
        System.out.println("task01 running...");
    }
}

//进行定时任务的调用
public class TaskTest {
    public static void main(String[] args) {

        Timer timer = new Timer();
        //delay:延迟时间(ms),period:执行周期
        timer.schedule(new Task(),1000,5000);

        new Timer().schedule(new Task(){
            @Override
            public void run() {
                System.out.println("running...stop....");
            }
        },1000,2000);  
    }
}

ScheduledExecutorService接口

ScheduledThreadPoolExecutor作为ScheduledExecutorService接口的实现类,提供了对应的schedule方法:

public class ScheduledThreadPoolExecutorTest {
    public static void main(String[] args) {
        ScheduledThreadPoolExecutor task = (ScheduledThreadPoolExecutor) Executors.newScheduledThreadPool(5);
        //5个线程
        for (int i = 0; i < 5; i++) {
            //同一时间有五条任务进行调度
            task.schedule(()->{
                System.out.println(Thread.currentThread().getName() + "running...");
            },1000, TimeUnit.MILLISECONDS);
        }
    }
}

QuartZ

Quartz是一个完全由Java编写的开源作业调度框架,为在Java应用程序中进行作业调度提供了简单却强大的机制。Quartz允许开发人员根据时间间隔来调度作业。它实现了作业和触发器的多对多的关系,还能把多个作业与不同的触发器关联。

在QuartZ中存在主要的三个核心类:

  • Scheduler:调度器。所有的调度都是由它控制。其中,Trigger和JobDetails可以注册打到Scheduler中,两者在Scheduler中拥有各自的组及名称,且保证Trigger和JobDetails的组和名称保证唯一性。

  • Trigger: 定义触发的条件。主要有SimpleTriggerCronTrigger这两个子类。当仅需触发一次或者以固定时间间隔周期执行,SimpleTrigger是最适合的选择;而CronTrigger则可以通过Cron表达式定义出各种复杂时间规则的调度方案:如每早晨9:00执行,周一、周三、周五下午5:00执行等

  • JobDetail & Job: JobDetail 定义的是任务数据,而真正的执行逻辑是在Job中。

    • 为什么设计成JobDetail + Job,不直接使用Job?这是因为任务是有可能并发执行,如果Scheduler直接使用Job,就会存在对同一个Job实例并发访问的问题。而JobDetail & Job 方式,sheduler每次执行,都会根据JobDetail创建一个新的Job实例,这样就可以规避并发访问的问题。

    如果你是SpringBoot2.0以上的版本,那么只需引入以下依赖即可:

 <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
 </dependency>

我们先定义一个TastJob

public class TaskJob implements Job {

    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        System.out.println("定时任务开始执行中...");
        //获取jobDetails和jobDataMap
        JobDetail jobDetail = jobExecutionContext.getJobDetail();
        JobDataMap jobDataMap = jobDetail.getJobDataMap();
        //执行定时任务...这里打印对应的Job细节信息
        System.out.println("name: " + jobDataMap.get("name") +
                "job: " + jobDataMap.get("job") +
                "level:" + jobDataMap.get("level"));
    }
}

然后定义TestSchedule进行任务调度:

public class TestSchedule {
    public static void main(String[] args) throws SchedulerException {
        //第一步:获取调度工厂
        StdSchedulerFactory factory = new StdSchedulerFactory();
        //第二步:通过工厂获取调度器
        Scheduler scheduler = factory.getScheduler();
        //第三步:创建对应的JobDetails
        JobDetail jobDetail= JobBuilder.newJob(TaskJob.class)
                .withIdentity("details","group1")
                .withDescription("this is a jobDetail")
                .build();
        //设置具体的工作参数
        jobDetail.getJobDataMap().put("name","zhuzhu");
        jobDetail.getJobDataMap().put("job","job1");
        jobDetail.getJobDataMap().put("level","level1");

        //设置开始时间
        Long currentTime = System.currentTimeMillis() + 6 * 1000L;
        Date time = new Date(currentTime);
        //第四步:创建Trigger(调度规则)
        Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("trigger","group01")
                //任务开始时间
                .startAt(time)
                .withDescription("this is a trigger")
                //调度设置1:使用SimpleScheduleBuilder调度器,每隔2秒重复执行3次
                .withSchedule(SimpleScheduleBuilder.simpleSchedule().
                        withIntervalInSeconds(2).withRepeatCount(3))
                //调度设置2:使用CronScheduleBuilder调度器,每5秒调度一次
                //.withSchedule(CronScheduleBuilder.cronSchedule("0/5 * * * * ?"))
                .build();
        scheduler.scheduleJob(jobDetail,trigger);
        scheduler.start();
    }
}

注意两个注解:

  • @DisallowConcurrentExecution:禁止并发地执行同一个job定义,即进行Job实例创建的是同一个实例,而非每一次创建一个不同的实例
  • @PersistJobDataAfterExecution:持久化JobDetail中的JobDataMap,公用同一个JobDetailMap实例

SpringTask

SpringTask默认在无任何第三方依赖的情况下使用spring-context模块下提供的定时任务工具Spring Task。我们只需要使用@EnableScheduling注解就可以开启相关的定时任务功能。如:

@SpringBootApplication
@EnableScheduling
public class SpringbootScheduleApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringbootScheduleApplication.class, args);
    }
}

此时,只需要定义一个Spring Bean,然后定义具体的定时任务逻辑方法就可以使用@Scheduled注解标记该方法即可

package cn.felord.schedule.task;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@Component
public class TaskService {

    @Scheduled(fixedDelay = 1000)
    public void task() {
        System.out.println("Thread Name : "
                    + Thread.currentThread().getName() + "  i am a task : date ->  "
                    + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));

    }
}

其中,@Scheduled中提供了四种属性:

  • cron表达式
  • fixedDelay : 定时任务开启是根据上次的任务结束的时间开始计算的,只需要关注上一次任务的结束时间即可
  • fixedRate : 由于默认SpringBoot的定时任务是单线程执行的,这里下一次开始和上一次开始的间隔是一定的,如果本次任务超时完成,则下一次任务的等待时间就会被压缩设置阻塞。
  • initDelay:初始化延迟时间,也就是第一次延迟执行的时间,该参数对cron属性无效,只能配合后两个实现。
弊端:
  • 多线程下影响定时策略
  • 默认不支持分布式,在分布式环境下,SpringTask的定时任务不支持集群配置,如果配置到多个节点上,各个节点上并不存在任何通讯机制,集群的节点之间是不会共享任务信息的,每一个节点上的任务都会按时执行,导致任务的重复执行。此时可以使用QuartZ,XXL-Job,Elastic-Job。当然可以借助Zookeeper,redis来实现分布式锁来处理各种节点的协调问题。或者将所有的定时任务抽成单独的服务单独部署

定时任务不支持集群配置,如果配置到多个节点上,各个节点上并不存在任何通讯机制,集群的节点之间是不会共享任务信息的,每一个节点上的任务都会按时执行,导致任务的重复执行。此时可以使用QuartZ,XXL-Job,Elastic-Job。当然可以借助Zookeeper,redis来实现分布式锁来处理各种节点的协调问题。或者将所有的定时任务抽成单独的服务单独部署

解决方式:

为了让每一个定时任务在不同线程运行,不受干扰,可以添加以下配置类:

@Configuration
@EnableAsync
public class AsyncConfig {
     /*
    此处成员变量应该写到我们的properties配置文件当中,然后使用@Value从配置中读取
     */
    private int corePoolSize = 10;
    private int maxPoolSize = 200;
    private int queueCapacity = 10;
    
    @Bean
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(corePoolSize);
        executor.setMaxPoolSize(maxPoolSize);
        executor.setQueueCapacity(queueCapacity);
        executor.initialize();
        return executor;
    }
}

然后在定时任务的类或者方法上添加@Async 。最后重启项目。此时,每一个任务都是在不同的线程中。

个人建议使用QuartZ(SpringBoot2.0以上版本)或者SpringTask进行定时任务的编写

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值