Java中定时任务Timer引发的生产问题
该生产问题是其他项目组中存在的,但因为当时我有参与这模块的代码评审,当时也指出来该地方有问题,建议修改为ScheduledExecutorService或多测试。但是后面重视程度不够,导致引发了生产问题。
问题代码分析
其中有如下部分代码。
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
try {
//TODO
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, 10*1000, 5000);
通过这种单线程的方式实现,在存在多个定时任务的时候便会存在问题:若任务A执行时间过长,将导致任务B延迟了启动时间!
另外一个问题,应该是属于设计的问题:若任务线程在执行队列中某个任务时,该任务抛出异常,将导致线程因跳出循环体而终止,即Timer停止了工作!
所以,这个地方一旦抛出异常,则导致线程终止,我们线上就是因为某个数据执行时抛出了异常,导致整批数据终止,这是我们不愿意看到的。该schedule的API注释是有说明的。建议用ScheduledThreadPoolExecutor替换,但是使用它也有很多注意事项。
ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor继承自ThreadPoolExecutor,所以本质上说ScheduledThreadPoolExecutor还是一个线程池。它也有coorPoolSize和workQueue,接受Runnable的子类作为任务。
特殊的地方在于它实现了自己的工作队列DelayedWorkQueue,该任务队列的作用是按照一定顺序对队列中的任务进行排序。比如,按照距离下次执行时间的长短的升序方式排列,让需要尽快执行的任务排在队首,“不那么着急”的任务排在队列后方,从而方便线程获取到“应该”被执行的任务。除此之外,ScheduledThreadPoolExecutor还在任务执行结束后,计算出下次执行的时间,重新放到工作队列中,等待下次调用。
ScheduledThreadPoolExecutor可以说是Timer的多线程实现版本,连JDK官方都推荐使用ScheduledThreadPoolExecutor替代Timer。下面是替代的方式。
ScheduledExecutorService pool = Executors.newSingleThreadScheduledExecutor();
//使用
pool.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
//TODO
}
}, 0 ,5, TimeUnit.SECONDS);
另外,当我们用springboot时,也可以用这种方式。
使用 @Schedule注解的方式
核心类:@EnableScheduling @Scheduled
@Scheduled(fixedDelay = 5000)
public void doSomething(){
//TODO
}
使用 SchedulingConfigurer接口 + runable线程实现
核心类:Executor,ScheduledThreadPoolExecutor,SchedulingConfigurer,Runnable
public class Task extends CommonTask implements Runnable {
//1.业务代码
public void run() {}
}
//2.任务执行
public void configureTasks(final ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(executor);
platform.getTasks().forEach(final task -> taskRegistrar.addTriggerTask(task, triggerContext -> {
ConTrigger trigger = new CronTrigger(task.getCron());
Date nextExecDate = trigger.nextExecutionTime(triggerContext);
return nextExecDate;
}));
}
使用quartz框架Api实现
- 使用springboot整合quartz框架方式
核心类:Job,AdaptableJobFactory,SchedulerFactoryBean,Scheduler,TriggerKey。
三种方式的使用比较。
功能 | @Scheduled注解 | SchedulingConfigure接口+runable线程 | quartz |
---|---|---|---|
并行运行 | 需要新增配置文件 | 需要配置注解 | 默认并发 |
动态新增 | 复杂,暂无好方案 | 复杂,暂无好方案 | 使用自带API |
动态修改 | 使用${} cron表达式 | 使用组合模式,修改线程 | 使用自带API |
可监控信息 | 复杂,暂无好方案 | 可以使用api获取简单的信息,不完善 | 使用自带API,但是能获取信息也相对较少 |
结合几种实现方式和实际需求的覆盖率,使用quartz+多线程的方式应该是最合适的方案,不过在监控信息方面,所有的方案均不能满足实际需求,所以,需要额外的引入,日志,并且在业务端手动的新增监控信息,才能达到详细的记录、监控的目的。