文章目录

  • 一.前言
  • 二.普通项目
  • 1.Timer
  • 2.ScheduledExecutorService
  • (1)scheduleAtFixedRate
  • (2)scheduleWithFixedDelay.
  • (3).对异常的处理
  • 三.Spring项目.
  • 1.Spring Task
  • 2.结合@EnableAsync使用
  • 四.总结


一.前言

   定时任务在工作中可以说是最常见的需求了,比如定时发送邮件,读取配置,清除缓存等等,这一切都离不开定时任务,所以熟练的掌握定时任务使用及其注意点是非常有必要的,本文将从不同项目环境演示其对应定时任务的使用和注意点.

二.普通项目

1.Timer

   在普通项目中,最常用的就是Timer了,它是Java自带的一种定时任务,可以很方便的进行使用.
在 java.util 包下,要跟 TimerTask 一起配合使用。
举例:

//设置定时任务
    TimerTask task = new TimerTask() {
         @Override
         public void run() {
         	// TODO Auto-generated method stub
	         System.out.println("运行定时任务......");
        }
    };
   Timer timer = new Timer();
   //1秒后执行,然后每隔两秒执行一次.
   timer.schedule(task,1000L,2000L);

   我们知道定时任务都是会启一个异步线程,去帮我们执行一些任务,那么在处理任何的定时任务时,一定要问自己两个问题:

  • 如果某个定时任务超时了,是会帮我们再起一个线程去执行,还是会一直等当前任务执行完毕再去执行?
  • 如果某个定时任务出现异常,会怎么处理?会不会影响其他任务的执行?

验证:
1.我们这里直接设定定时任务时间为3s,但是我们想让其每2s就执行一次定时任务,看看结果会怎么样

//设置定时任务
        TimerTask task = new TimerTask() {
            @Override
            public void run() {
                //故意设定执行时间>间隔时间
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程:"+Thread.currentThread().getName()+",当前执行时间:"+System.currentTimeMillis()/1000);
            }
        };
        Timer timer = new Timer();
        //1秒后执行,然后每隔两秒执行一次.
        timer.schedule(task,1000L,2000L);

输出结果:

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


可以看到,每个定时任务间隔时间是有3s,说明Timer只有一个异步线程去同步执行定时任务,如果一个任务延迟,会影响到其他的任务.

2.直接让定时任务出现异常,看看会怎么样?

//设置定时任务
        TimerTask task = new TimerTask() {
            @Override
            public void run() {
                //设置异常
                int i=2/0;
                System.out.println("线程:"+Thread.currentThread().getName()+",当前执行时间:"+System.currentTimeMillis()/1000);
            }
        };
        Timer timer = new Timer();
        //1秒后执行,然后每隔两秒执行一次.
        timer.schedule(task,1000L,2000L);

输出结果:

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


程序直接抛出异常,然后结束了,说明Timer对异常并没有进行任何的处理,直接向上抛出

结论:

  1. Timer中的定时任务只会启一个线程,也就是说如果某个任务因为一些异常原因延迟,将会影响到后面定时任务的执行.
  2. Timer中任一任务出现异常,会直接导致整个程序结束.

2.ScheduledExecutorService

   在了解了Timer后,我们知道它有些不足,是单线程去运行的,所以JDK又推出了一种结合线程池的定时任务类.即ScheduledExecutorService,针对每个线程任务,会指定线程池中的一个线程去执行.是Timer的更好的一个替代品.

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


主要方法:

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


这里不再演示一次性任务schedule()的使用,主要演示scheduleAtFixedRate()和scheduleWithFixedDelay()

(1)scheduleAtFixedRate

以给定的间隔去执行任务,当任务的执行时间过长,超过间隔时间后,下一次定时任务的执行会顺延至当前定时任务执行完毕之后.否则严格按照间隔时间去执行.
举例:

*/
    public class MyFutureTask2 {
        public static void main(String[] args) throws ExecutionException, InterruptedException {
         //创建一个线程池,以JVM可以利用的CPU数为核心线程数,并指定拒绝策略
                ScheduledThreadPoolExecutor scheduledExecutor = new ScheduledThreadPoolExecutor(Runtime.getRuntime().availableProcessors(),
                        new RejectedExecutionHandler() {
                            @Override
                            public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                                System.out.println("当前任务执行失败" + r);
                            }
                        });

                // 在指定0秒延迟后执行,之后每两秒执行一次,此时任务指定时间是大于间隔时间的
                scheduledExecutor.scheduleAtFixedRate(new ThreadRunnable(), 0, 2, TimeUnit.SECONDS);
        }
    }

    class ThreadRunnable implements Runnable{

        @Override
        public void run() {
            //设定执行时间>间隔时间
            System.out.println("线程:"+Thread.currentThread().getName()+",执行任务时间:"+ LocalDateTime.now());
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

输出结果:

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


可以看到,我们指定的任务间隔时间是2s,但是每一个任务要执行的花费时间为10s,这个时候定时任务就是顺延至当前任务执行完毕后,立即开始执行下一个任务.所以时间差值是10s.

(2)scheduleWithFixedDelay.

以给定的间隔时间执行任务,区别是这个间隔时间是从上一次定时任务执行完毕之后才开始算起的.
举例:
还是刚刚的场景,间隔2s,任务执行时间10s.

public class MyFutureTask2 {
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            //创建一个线程池,以JVM可以利用的CPU数为核心线程数,并指定拒绝策略
            ScheduledThreadPoolExecutor scheduledExecutor = new ScheduledThreadPoolExecutor(Runtime.getRuntime().availableProcessors(),
                new RejectedExecutionHandler() {
                    @Override
                    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                        System.out.println("当前任务执行失败" + r);
                    }
                });

            // 在指定1秒延迟后执行,之后每两秒执行一次,此时任务指定时间是大于间隔时间的
            scheduledExecutor.scheduleWithFixedDelay(new ThreadRunnable(), 0, 2, TimeUnit.SECONDS);
        }
    }

    class ThreadRunnable implements Runnable{

        @Override
        public void run() {
            //设定执行时间>间隔时间
            System.out.println("线程:"+Thread.currentThread().getName()+",执行任务时间:"+ LocalDateTime.now());
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

输出结果:

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


可以看到,任务的实际间隔时间达到了12s,也就是 任务执行的时间+指定的间隔时间

   从上面来看,ScheduledExecutorService是基于线程池实现的,相比Timer也有更强大的API,但是从上面输出结果看,其对于每个设定的定时任务其实还是同步执行的,区别是ScheduledExecutorService可以很方便的设置多个定时任务,如下图所示:

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

(3).对异常的处理

在Timer中,出现异常会导致整个程序终止,那么在ScheduledExecutorService中呢?
直接上代码演示:
这里定义了一个随机异常,当返回的布尔值为true时,程序出现异常.

public class MyFutureTask2 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建一个线程池,一JVM可以利用的CPU数为核心线程数,并指定线程工厂和拒绝策略
        ScheduledThreadPoolExecutor scheduledExecutor = new ScheduledThreadPoolExecutor(Runtime.getRuntime().availableProcessors(),
                new RejectedExecutionHandler() {
                    @Override
                    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                        System.out.println("当前任务执行失败" + r);
                    }
                });

        // 在指定0秒延迟后执行,之后每两秒执行一次
        scheduledExecutor.scheduleAtFixedRate(new ThreadRunnable(), 0, 2, TimeUnit.SECONDS);
    }
}

class ThreadRunnable implements Runnable{

    @Override
    public void run() {
        //设定随机异常
        Random random = new Random();
        boolean nextBoolean = random.nextBoolean();
        if(nextBoolean){
            System.out.println("程序开始出现异常...");
            int i=2/0;
        }
        System.out.println("线程:"+Thread.currentThread().getName()+",执行任务时间:"+ LocalDateTime.now());
    }
}

输出结果:

java 定时任务 线程 java程序定时任务_开发语言_08


在程序出现异常后,没有任何打印信息,并且定时任务也不会继续执行,所以ScheduledExecutorService对异常的处理也并不好,我们甚至不知道出现了什么情况,所以在使用ScheduledExecutorService的时候,一定要做好异常的捕获,否则定时任务不执行都不知道出现了什么情况

三.Spring项目.

在Spring 项目中给了一种更加简单的方式去启动定时任务,我们这里以Springboot项目举例进行说明.
其实它底层是也基于 JDK 的 ScheduledExecutorService实现的.

1.Spring Task

使用步骤:

  1. 引入依赖.
<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.retry</groupId>
            <artifactId>spring-retry</artifactId>
        </dependency>
  1. 配置类.
    重点:使用 @EnableScheduling注解启动定时任务功能,并实现自定义线程池.
@Configuration
     //该注解可以启动TaskScheduling,实现SchedulingConfigurer是为了对你的定时任务有一些个性化的设置
     @EnableScheduling
      public class TaskSchedulerConfig implements SchedulingConfigurer {

          @Override
           public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
                //实现定时任务必须要有线程池,这里即传入一个线程池,是一个自定义方法,在下面实现
                scheduledTaskRegistrar.setScheduler(schedulerThreadPool());
           }
          //里面标注方法的意思就是在这个bean destroy的时候,调用ScheduledThreadPoolExecutor类的shutdown方法优雅的关闭线程池
          @Bean(destroyMethod = "shutdown")
          public ScheduledThreadPoolExecutor schedulerThreadPool() {
               /*ScheduledThreadPoolExecutor这个类是实现了定时任务的线程池
               Runtime类解析,每一个java运行程序都有一个Runtime类实例,使当前运行程序能够与运行环境相关联,getRuntime方法返回当前
                运行程序的Runtime对象,avaliableProcessors方法返回可用处理器的数目,用返回的处理器的数目充当corePoolSize
                */
               return new ScheduledThreadPoolExecutor(Runtime.getRuntime().availableProcessors(),
                    new RejectedExecutionHandler() {
                        @Override
                        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                            System.out.println("当前任务执行失败"+r);
                        }
                     });

           }
      }
  1. 然后就可以使用定时任务了.
    重点:使用@Scheduled注解真正开启一个定时任务.注意定时任务类一定要注入到Spring工厂中
@Component
      public class Scheduler {
           /**
           初始化延迟3秒后执行,  
           fixedDelay则和scheduleWithFixedDelay方法含义是一样的
           fixedRate和scheduleAtFixedRate方法含义是一样的
           */
            
           @Scheduled(initialDelay = 3000,fixedDelay = 3000)           
            public void execute(){
               System.out.println("定时任务执行.....");
  
            }
          
          @Scheduled(cron="0 10 23 20 10 7")     //cron表达式,比较常用,最下面有详细用法.
           public void execute2(){
    
              System.out.println("定时任务执行2...");
          }
       }

   这里多了一个可以使用cron表达式进行定时任务的配置,cron表达式可以定义更加灵活的定时配置,比如说周一到周五每天上午10:15执行任务,等等.如果不太了解的,可以参考这个讲解cron表达式注意:Spring Task底层使用的是JDK的ScheduledExecutorService,所以它的每个定时任务一定也是同步执行的,也就是说当间隔时间达到之后,如果上一个定时任务没有执行完毕,会等待上一个定时任务执行完之后才开启下一个

此外,Spring Task是会抛出异常的,并且之后的定时任务还会继续执行,不会受到影响,这个是比较好的

2.结合@EnableAsync使用

   如果我们的业务场景中,需要完全忽略任务的执行时间,假如上一个任务没有执行完毕的情况下,直接新启一个线程去执行,那么我们就可以结合@EnableAsync去使用.

举例:

(1).启用异步线程功能.增加@EnableAsync注解

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


(2).在定时任务的方法上增加@Async注解.

@Async
    @Scheduled(initialDelay = 1000,fixedRate = 2000)
    public  void test() throws InterruptedException {
        System.out.println("线程:"+Thread.currentThread().getName()+",当前执行时间:"+ LocalTime.now());
        Thread.sleep(5000);

    }

输出结果:

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


可以看到,即使定时任务的执行时间是5s,间隔时间是2s,每次也都会新启一个线程去执行任务,但是要注意,使用此方式,会默认使用异步任务对应的默认线程池SimpleAsyncTaskExecutor,而不是定时任务中的线程池ScheduledThreadPoolExecutor,所以在此方式下,还需要自定义一下异步任务的线程池

@Bean
    public ThreadPoolTaskExecutor taskExecutor(){
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setCorePoolSize(10);
        taskExecutor.setMaxPoolSize(10);
        taskExecutor.setQueueCapacity(200);
        taskExecutor.setKeepAliveSeconds(60);
        taskExecutor.setThreadNamePrefix("自定义-");
        taskExecutor.setAwaitTerminationSeconds(60);
        //使用自定义拒绝策略,或者自带的几种拒绝策略
        taskExecutor.setRejectedExecutionHandler((runable,threadPoolExecutor)->{
            //TODO 自定义的拒绝策略
            System.out.println("当前任务执行失败:"+runable);
        });
        return taskExecutor;
    }

增加自定义异步线程池后的输出结果:

java 定时任务 线程 java程序定时任务_开发语言_11

上面所说的定时任务其实只支持单点的定时任务,如果要使用分布式定时任务,可以使用业界比较主流的es-job.xxl-job等.es-job是使用ES做辅助处理,xxl-job则是使用Mysql做辅助处理,这里暂时不再详细说明,待补充博客.

四.总结

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