java简单实现定时任务

  • 使用Timer
  • 使用ScheduledThreadPool
  • 使用Spring quartz



使用Timer

IDEA、 JDK1.8、 Spring boot
demo代码如下:

package com.momomian.learn.code.admin.task;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.Calendar;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

@Component
public class DemoTimerTask {

    private Timer timer;
    private boolean timerRunning = false;

    @PostConstruct
    public void init(){
        timer = new Timer("Demo-Timer-Task");
        Date firstTime = getFirstTime();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                timerRunning = true;
                try {
                    performingTasks();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                timerRunning = false;
            }
        }, firstTime, 1000);
    }

    public Date getFirstTime(){
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(new Date());
        calendar.add(Calendar.DAY_OF_MONTH, 0);
        calendar.set(Calendar.HOUR_OF_DAY, 0);
        calendar.set(Calendar.MINUTE, 0);
        calendar.set(Calendar.SECOND, 0);
        return calendar.getTime();
    }

    public void  performingTasks(){
        System.out.println("Are you ok?"+System.currentTimeMillis());
    }
}

解析:
  timer.schedule方法接受一个TimerTask 对象,firstTime是其第一次触发的时间。如果程序运行时的时间大于等于这个时间,那么就会执行任务。period是指定的时间间隔,单位是毫秒。

public void schedule(TimerTask task, Date firstTime, long period) {…}

  getFirstTime方法是获取一个初次执行时间。你可以用calendar.set方法设置具体时间,或者add方法增加一个相对的时间。最后得到初次执行的时间。

我们设定间隔为1000毫秒,从运行的结果看来,每秒钟执行一次,似乎延迟约半毫秒到一毫秒。这种延迟是叠加的。时间拖得越长,与预定的执行之间可能会造成偏差。

java 定时任务线程 java程序定时任务_定时任务


  如果任务时间超过了我们设定的间隔时间,Timer会怎么处理呢?我对timer对象在此执行一个schedule方法去new一个TimerTask 作为定时任务2。发现在这个任务没有执行之前,并不会去执行定时任务1。也就是说会导致定时任务1没有按时执行任务。这是因为一个Timer只开启一个线程

  你自然可以每次一个新的任务就去写一个类创建一个新的Timer,这样就可以避免这个问题。

  但是注意看看你的项目中用了多少Timer,如果你处理的不好,太多的线程数将可能让你的系统崩溃。我们需要一个线程池去管理。

  从阿里巴巴的代码规范看它要求我们使用ScheduledThreadPool去做定时任务。

如果你正在使用IDEA,并且使用了Alibaba Java Coding Guidelines插件,你在编写上述代码的时候Timer就会出现一个提示:

多线程并行处理定时任务时,Timer运行多个TimeTask时,只要其中之一没有捕获抛出的异常,其它任务便会自动终止运行,使用ScheduledExecutorService则没有这个问题。

实验做一下很简单,在上述代码中再增加一个schedule执行。

timer.schedule(new TimerTask() {
           @Override
           public void run() {
               System.out.println("strat");
               timerRunning = true;
                performingTasks2();
               timerRunning = false;
           }
       }, firstTime, 1);

这里我们不去try,catch,那么出现异常的时候,这个timer将停止工作。

public void  performingTasks2(){
       String name = null;
       String surname = getSurname(name);
       System.out.println(surname);
}
public String getSurname(String name){
        String[] split = name.split("-");
        return split[0];
}

使用ScheduledThreadPool

从阿里的规约上,给出了一个例子:

//org.apache.commons.lang3.concurrent.BasicThreadFactory
 ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1,
 new BasicThreadFactory.Builder().namingPattern(“example-schedule-pool-%d”).daemon(true).build());
 executorService.scheduleAtFixedRate(new Runnable() {
 @Override
 public void run() {
 //do something
 }
 },initialDelay,period, TimeUnit.HOURS);

其中BasicThreadFactory类来自:org.apache.commons.lang3.concurrent.BasicThreadFactory

maven添加依赖

<dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-lang3</artifactId>
                <version>3.8</version>
            </dependency>

根据规约的提示我们写一个简单的demo (JDK1.8+)

@Component
public class ScheduledThreadPoolTaskTimer {

    @PostConstruct
    public void init(){
        ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1,
                new BasicThreadFactory.Builder().namingPattern("task-timer-pool-%d").daemon(true).build());
        executorService.scheduleAtFixedRate(this::performingTasks,0,1000, TimeUnit.MILLISECONDS);
    }
    public void  performingTasks(){
        System.out.println("Are you ok?"+System.currentTimeMillis());
    }
}

这里我们使用了BasicThreadFactory去创建线程,也就是采用线程池的方式。scheduleAtFixedRate方法参数如下

  • @param command 要执行的任务
  • @param initialDelay 延迟首次执行的时间
  • @param period 连续执行之间的时间间隔
  • @param unit initialDelay和period参数的时间单位

我们同样设定间隔为1000毫秒,

java 定时任务线程 java程序定时任务_java 定时任务线程_02


结果可以看出,这里的间隔时间虽然有一个毫秒的误差,但是没有误差的叠加性。

并且我们可以做之前的测试。

@PostConstruct
    public void init(){
        ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1,
                new BasicThreadFactory.Builder().namingPattern("task-timer-pool-%d").daemon(true).build());
        executorService.scheduleAtFixedRate(this::performingTasks,0,1000, TimeUnit.MILLISECONDS);
	    executorService.scheduleAtFixedRate(this::performingTasks2,0,1000, TimeUnit.MILLISECONDS);
    }
    public void  performingTasks(){
        System.out.println("Are you ok?"+System.currentTimeMillis());
    }
    public void  performingTasks2(){
        String name = null;
        String surname = getSurname(name);
        System.out.println(surname);
    }
    public String getSurname(String name){
        String[] split = name.split("-");
        return split[0];
    }

发现尽管第二个任务异常没有捕获,没有执行。但是第一个任务依然正常执行。

ScheduledThreadPoolExecutor new的时候使用的构造方法可以传入一个corePoolSize。用于指定线程池的可以容纳多少线程。

使用Spring quartz

如果你使用的是spring boot,可以用这个方案

首先在入口类中添加注解@EnableScheduling

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

实现类demo:

@Component
public class QuartzTask {

     public boolean isAllowRunEat = true ;
    @Scheduled(cron = "*/5 * * * * ?")
    public void taskToEat(){
        try{
            if(isAllowRunEat){
                performingEatTask();
            }
           
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    @Scheduled(cron = "*/10 * * * * ?")
    public void taskToDrink(){
        try{
            performingDrinkTask();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    public void  performingEatTask(){
        System.out.println("eat eat eat "+System.currentTimeMillis());
    }
    public void  performingDrinkTask(){
        System.out.println("drink drink drink "+System.currentTimeMillis());
    }
}

这种方式是十分简单的,也是避免了Timer的问题,唯一需要讲解的是cron 表达式。这个是一个时间相关的表达式:
由至少6个时间元素组成,用空格分开。从左到右是:
秒(0-59)、分(0-59)、时(0-23)、日(0-31)、月(0-11)、星期(1-7)、年(1970-2099) 其中其中月份中的日和星期中的日存在冲突,选择其一配置即可。
其中每个元素可以是:
1:值 如:1
2:连续区间 表达式 : n-m
3:间隔时间 /n 表示每隔n…
4:列表 (1,2,3)
5:通配符
注意:?号仅用于天月和天星期,表示不指定
例子:

1、0 0 5 * * ? 每天凌晨5点执行一次
2、 0 0 1,3,4 * * ? 每天的1点3点4点执行一次
3、*/5 * * * * ? 每隔5秒生成一次

这个表达式可以说是很强大了。懒得学的人可以使用以下网站:
cron表达式在线生成链接

isAllowRunEat 这个变量的目的是控制定时任务的触发条件。如果你有需求通过其他方法去开启/关闭这个任务,你可以将其配置到配置文件中,或者你有需求可以将其在后台实现可以人工配置。这是需求允许情况下的简单方案。
  到这里我们已经学会了三种定时任务简单的使用。我这边写的例子都是简单的。你应该为你的项目需求考虑更加优雅的实现方式,实现重用。像上面一个类中写两个任务是不可取的方式。保证单一职责是重要的。
更加完善的方案可以参考:
懂点IT 的文章,链接附上:

草草结尾。我要回家偷懒,拜拜。


分享个人喜欢的句子:

胜利…失败…这些词语没有任何意义。生命位于这些意象之下,并且已经准备好新的意象。一场胜利削弱了一群怀抱着同样信仰的人,一场败仗却唤醒了另一群。-让-保罗·萨特