Java---实现定时任务

Posted 高高for 循环

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java---实现定时任务相关的知识,希望对你有一定的参考价值。

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录


Java实现定时任务

为什么要使用定时器呢?

  • 比如说一个web应用,如果这个应用规模很大,那它的日志数据是不是很多。如果一直存下来服务器的存储量怕是不行吧,需要隔一段时间删除,那么就需要一个线程每隔一段时间去删除日志数据。

一、Timer

java–Timer 定时器

Timer是JAVA自带的定时任务类,实现如下:

public class MyTimerTask     
    public static void main(String[] args)         
        // 定义一个任务       
        TimerTask timerTask = new TimerTask()             
        @Override            
            public void run()                 
            System.out.println("打印当前时间:" + new Date());    
                   
         ;        
        // 计时器       
        Timer timer = new Timer();       
        // 开始执行任务 (延迟1000毫秒执行,每3000毫秒执行一次)        
        timer.schedule(timerTask, 1000, 3000);    
    

Timer 优缺点分析

  • 优点是使用简单,缺点是当添加并执行多个任务时,前面任务的执行用时和异常将影响到后面任务,这边建议谨慎使用

缺点:

  1. Timer 的背后只有一个线程,不管有多少个任务,都只有一个工作线程串行执行,效率低下
  2. 受限于单线程,如果第一个任务逻辑上死循环了,后续的任务一个都得不到执行
  3. 依然是由于单线程,任一任务抛出异常后,整个 Timer 就会结束,后续任务全部都无法执行

二、ScheduledExecutorService

ScheduledExecutorService 即是 Timer 的替代者,JDK 1.5 并发包引入,是基于线程池设计的定时任务类。每个调度任务都会分配到线程池中的某一个线程去执行,任务就是并发调度执行的,任务之间互不影响

Java 5.0引入了java.util.concurrent包,其中的并发实用程序之一是ScheduledThreadPoolExecutor ,它是一个线程池,用于以给定的速率或延迟重复执行任务。它实际上是Timer/TimerTask组合的更通用替代品,因为它允许多个服务线程,接受各种时间单位,并且不需要子类TimerTask (只需实现Runnable)。使用一个线程配置ScheduledThreadPoolExecutor使其等效于Timer 。

提升–16—线程池–02—线程池7大参数、Executors工具类

public class MyScheduledExecutorService     
    public static void main(String[] args)         
        // 创建任务队列   10 为线程数量      
        ScheduledExecutorService scheduledExecutorService = 
                Executors.newScheduledThreadPool(10); 
        // 执行任务      
        scheduledExecutorService.scheduleAtFixedRate(() ->           
            System.out.println("打印当前时间:" + new Date());      
        , 1, 3, TimeUnit.SECONDS); // 1s 后开始执行,每 3s 执行一次   
 
  

三、Spring Task

Spring系列框架中Spring Framework自带的定时任务,

使用上面两种方式,很难实现某些特定需求,比如每周一执行某任务,但SpringTask可轻松实现。

以SpringBoot为例来实现:

1、开启定时任务

在SpringBoot的启动类上声明 @EnableScheduling:

@SpringBootApplication
@EnableScheduling //开启定时任务
public class DemoApplication   
     // --  -- 

2、添加定时任务

只需使用@Scheduled注解标注即可,

如果有多个定时任务,可以创建多个@Scheduled标注的方法,示例如下:

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component // 把此类托管给 Spring,不能省略
public class TaskUtils     
    // 添加定时任务    
    @Scheduled(cron = "30 40 23 0 0 5") // cron表达式:每周一 23:40:30 执行    
    public void doTask()        
        System.out.println("我是定时任务~");    
    

Spring Boot 启动后会自动加载并执行定时任务,无需手动操作。

3.可以通过@EnableAsync和 @Async开启多线程

import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;

@Component
@EnableAsync  // 开启异步多线程
@EnableScheduling
public class Schedule 

    @Async
    @Scheduled(fixedRate = 2000L)
    public void task() 
        System.out.println("当前线程:" + Thread.currentThread().getName() + " 当前时间" + LocalDateTime.now());
    



使用@EnableAsync注解后,默认情况下,Spring将搜索关联的线程池定义:上下文中的唯一org.springframework.core.task.TaskExecutor的bean,或者名为“taskExecutor”的java.util.concurrent.Executor的bean。如果两者都无法解析,则将使用org.springframework.core.task.SimpleAsyncTaskExecutor来处理异步方法调用。

TaskExecutor实现为每个任务启动一个新线程,异步执行它。 支持通过“concurrencyLimit”bean 属性限制并发线程。默认情况下,并发线程数是无限的,所以使用默认的线程池有导致内存溢出的风险。

注意:刚才的运行结果看起来是线程复用的,而实际上此实现不重用线程!应尽量实现一个线程池TaskExecutor,特别是用于执行大量短期任务。不要使用默认SimpleAsyncTaskExecutor。

import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.concurrent.Executor;

@Component
@EnableAsync
@EnableScheduling
public class Schedule 

    @Async
    @Scheduled(fixedRate = 2000L)
    public void task() 
        System.out.println("当前线程:" + Thread.currentThread().getName() + " 当前时间" + LocalDateTime.now());
    


    @Bean("taskExecutor")
    public Executor taskExecutor() 
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setCorePoolSize(10);
        taskExecutor.setMaxPoolSize(50);
        taskExecutor.setQueueCapacity(200);
        taskExecutor.setKeepAliveSeconds(60);
        taskExecutor.setThreadNamePrefix("自定义-");
        taskExecutor.setAwaitTerminationSeconds(60);
        return taskExecutor;
    


Cron 表达式

  • Spring Task 的实现需要使用 cron 表达式来声明执行的频率和规则,cron 表达式是由 6 位或者 7位组成的(最后一位可以省略),每位之间以空格分隔,每位从左到右代表的含义如下:


其中 * 和 ? 号都表示匹配所有的时间。

cron 表达式在线生成地址:https://cron.qqe2.com/

四、Quartz框架实现

除了JDK自带的API之外,我们还可以使用开源的框架来实现,比如Quartz。

Quartz是Job scheduling(作业调度)领域的一个开源项目,Quartz既可以单独使用也可以跟spring框架整合使用,在实际开发中一般会使用后者。使用Quartz可以开发一个或者多个定时任务,每个定时任务可以单独指定执行的时间,例如每隔1小时执行一次、每个月第一天上午10点执行一次、每个月最后一天下午5点执行一次等。

Quartz通常有三部分组成:

  • 调度器(Scheduler)、
  • 任务(JobDetail)、
  • 触发器(Trigger,包括SimpleTrigger和CronTrigger)。

1.依赖:

<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz</artifactId>
    <version>2.3.2</version>
</dependency>
<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz-jobs</artifactId>
    <version>2.3.2</version>
</dependency>

2.定义执行任务的Job

这里要实现Quartz提供的Job接口:

public class PrintJob implements Job 
    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException 
        System.out.println(new Date() + " : 任务「PrintJob」被执行。");
    


3.创建Scheduler和Trigger,并执行定时任务:

public class MyScheduler 

    public static void main(String[] args) throws SchedulerException 
        // 1、创建调度器Scheduler
        SchedulerFactory schedulerFactory = new StdSchedulerFactory();
        Scheduler scheduler = schedulerFactory.getScheduler();
        // 2、创建JobDetail实例,并与PrintJob类绑定(Job执行内容)
        JobDetail jobDetail = JobBuilder.newJob(PrintJob.class)
                .withIdentity("job", "group").build();
        // 3、构建Trigger实例,每隔1s执行一次
        Trigger trigger = TriggerBuilder.newTrigger().withIdentity("trigger", "triggerGroup")
                .startNow()//立即生效
                .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                        .withIntervalInSeconds(1)//每隔1s执行一次
                        .repeatForever()).build();//一直执行

        //4、Scheduler绑定Job和Trigger,并执行
        scheduler.scheduleJob(jobDetail, trigger);
        System.out.println("--------scheduler start ! ------------");
        scheduler.start();
    


执行程序,可以看到每1秒执行一次定时任务。

在上述代码中,其中Job为Quartz的接口,业务逻辑的实现通过实现该接口来实现。

JobDetail绑定指定的Job,每次Scheduler调度执行一个Job的时候,首先会拿到对应的Job,然后创建该Job实例,再去执行Job中的execute()的内容,任务执行结束后,关联的Job对象实例会被释放,且会被JVM GC清除。

Trigger是Quartz的触发器,用于通知Scheduler何时去执行对应Job。SimpleTrigger可以实现在一个指定时间段内执行一次作业任务或一个时间段内多次执行作业任务。

CronTrigger功能非常强大,是基于日历的作业调度,而SimpleTrigger是精准指定间隔,所以相比SimpleTrigger,CroTrigger更加常用。CroTrigger是基于Cron表达式的。

常见的Cron表达式示例如下:


可以看出,基于Quartz的CronTrigger可以实现非常丰富的定时任务场景。

五、使用 Redis 来实现定时任务

上面的方法都是关于单机定时任务的实现,如果是分布式环境可以使用 Redis 来实现定时任务。

  • 使用 Redis 实现延迟任务的方法大体可分为两类:通过 ZSet 的方式和键空间通知的方式。

1、ZSet 实现方式

通过 ZSet 实现定时任务的思路是,将定时任务存放到 ZSet 集合中,并且将过期时间存储到 ZSet 的 Score 字段中,然后通过一个无线循环来判断当前时间内是否有需要执行的定时任务,如果有则进行执行,具体实现代码如下:

import redis.clients.jedis.Jedis;
import utils.JedisUtils;
import java.time.Instant;
import java.util.Set;
public class DelayQueueExample         
    private static final String _KEY = "DelayQueueExample";        
    public static void main(String[] args) throws InterruptedException         
        Jedis jedis = JedisUtils.getJedis();        
        // 30s 后执行        
        long delayTime = Instant.now().plusSeconds(30).getEpochSecond();       
        jedis.zadd(_KEY, delayTime, "order_1");        
        // 继续添加测试数据        
       jedis.zadd(_KEY, Instant.now().plusSeconds(2).getEpochSecond(), "order_2");       
      jedis.zadd(_KEY, Instant.now().plusSeconds(2).getEpochSecond(), "order_3");        
      jedis.zadd(_KEY, Instant.now().plusSeconds(7).getEpochSecond(), "order_4");        
     jedis.zadd(_KEY, Instant.now().plusSeconds(10).getEpochSecond(), "order_5");        
        // 开启定时任务队列        
        doDelayQueue(jedis);    
        
    /**     
    * 定时任务队列消费     
    * @param jedis Redis 客户端     
    */    
    public static void doDelayQueue(Jedis jedis) throws InterruptedException         
        while (true)             
            // 当前时间            
            Instant nowInstant = Instant.now();            
            long lastSecond = nowInstant.plusSeconds(-1).getEpochSecond(); 
            // 上一秒时间            
            long nowSecond = nowInstant.getEpochSecond();            
            // 查询当前时间的所有任务            
            Set data = jedis.zrangeByScore(_KEY, lastSecond, nowSecond);            
            for (String item : data)                 
            // 消费任务                
            System.out.println("消费:" + item);            
                    
        // 删除已经执行的任务            
        jedis.zremrangeByScore(_KEY, lastSecond, nowSecond);            
        Thread.sleep(1000); // 每秒查询一次        
            
    

2、键空间通知

我们可以通过 Redis 的键空间通知来实现定时任务,它的实现思路是给所有的定时任务设置一个过期时间,等到了过期之后,我们通过订阅过期消息就能感知到定时任务需要被执行了,此时我们执行定时任务即可。

默认情况下 Redis 是不开启键空间通知的,需要我们通过 config set notify-keyspace-events Ex 的命令手动开启,开启之后定时任务的代码如下:

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPubSub;
import utils.JedisUtils;
public class TaskExample     
    public static final String _TOPIC = "__keyevent@0__:expired"; // 订阅频道名称   
    public static void main(String[] args)        
        Jedis jedis = JedisUtils.getJedis();       
        // 执行定时任务        
        doTask(jedis);    
       
     /**     
       * 订阅过期消息,执行定时任务     
       * @param jedis Redis 客户端     
       */    
    public static void doTask(Jedis jedis)         
        // 订阅过期消息        
        jedis.psubscribe(new JedisPubSub()             
            @Override            
 public void onPMessage(String pattern, String channel, String message)                 
            // 接收到消息,执行定时任务                
            System.out.println("收到消息:" + message);            
                        
        , _TOPIC);    
    

以上是关于Java---实现定时任务的主要内容,如果未能解决你的问题,请参考以下文章

java实现定时任务的三种方法

thinkphpqueue会重复执行吗

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

Java定时任务:利用java Timer类实现定时执行任务的功能

Java定时任务的常用实现

Linux环境下Shell调用MySQL并实现定时任务