Java中4类定时器比较总结

定时器总结

梳理了下目前用过的定时器实现方案:

  1. JDK的Timer包
  2. Thread
  3. ScheduledThreadPool
  4. Spring Timer(@Scheduled 推荐)

定时器的启动时间主要有以下:

  1. 随tomcat启动而启动——原生servlet(落后的方式)
  2. 随Spring启动而启动
  3. 其他时间(根据业务逻辑确定)

下面具体介绍每一种的实现及利弊,最后会说明分布式场景下定时器如何正确设置的思路,首先说明启动时机的实现(主要对前两个共性的启动时机进行说明):

1.1原生servlet(落后的方式)

最原始的解决方案,随着tomcat启动而启动,其加载顺序在spring之前,固在spring初始化之前定时器就启动了所以相关注入依赖会报NPE,只能用最原始的直接从上下文里去拿要的变量(不推荐)。

(SpringMVC)在build目录下的web.xml文件中需要添加以下配置(class具体放的是timer的全限定名)

<listener>
<listener-class>com.netease.pangu.guide.module.secondkill.service.ListenerService</listener-class>
</listener>

注意这里要修改的是build文件夹下的web.xml目录,一共有2个web.xml,另一个是打包后产生的在webapp->WEB-INF文件夹下,每次打包会根据build下的配置文件重新生成WEB-INF

当时第一次加这个配置就是这里出了问题,加到了WEB-INF下,那每次一打包都是用原来未修改的xml文件覆盖了当前修改的文件,根本就没有把这个配置加进去,找这个bug找了很久。

具体实现的话需要实现ServletContextListener接口,并重写里面的contextInitialized方法和contextDestroyed方法。

其中定时器的主要逻辑写在contextInitialized中,该方法会随tomcat的启动而启动,此处如果要用到Spring的依赖注入,需要直接从上下文去获取,真的非常原始了。。。

private static Timer timer  =  new Timer(true);//set timer as daemon

public class ListenerService implements ServletContextListener {

@Override
public void contextInitialized(ServletContextEvent event) {
//在listener中注入不可以用注解模式,要采用以下方式
final WebApplicationContext webApplicationContext =
WebApplicationContextUtils.getWebApplicationContext(event.getServletContext());
initRedis = webApplicationContext.getBean(InitRedis.class);

//本质是基于JDK的timer实现定时任务
TimedTask timedTask = new TimedTask(event.getServletContext(), event);
timer.schedule(timedTask, date,12 * 60 * 60 * 1000);

}

@Override
public void contextDestroyed(ServletContextEvent event) {
timer.cancel();
event.getServletContext().log("定时器销毁");
}

1.2 Spring启动后启动

其实从效果而言,Spring启动前启动与启动后再启动并无多大区别,而如果定时器启动时Spring已经启动了,那么就避免了手动去context中获取bean,代码简介多了(我一开始用第一种启动方法纯粹是搜的时候搜错了,第一种其实没有任何的必要,是一种落后的方法,网上的教程什么的完全没必要看,被误导了)
这种方法配置就更简单了,直接找到Spring的配置文件加入相关信息

<!-- 定时任务 -->
<task:executor id="executor" pool-size="5"/>
<task:scheduler id="scheduler" pool-size="10"/>
<task:annotation-driven executor="executor"
scheduler="scheduler"/>
<context:annotation-config/>
<bean class="org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor"/>
<context:component-scan base-package="com.netease.pangu.guide.module.seckill.service"/>

同时注意到此处有一个pool-size的参数,说明可以配置成线程池,也就没有了单线程timer的不足(后面具体说明)。


下面首先列举经典的jdk的timer使用

2.1JDK的Timer

使用非常简单,直接上代码(以之前蚂蚁二面笔试中的实现一个定时删除过期值得队列中的timer实现为例):

      Timer timer = new Timer();

timer.schedule(new TimerTask() {
@Override
public void run() {
try {
scanAndDelete();
} catch (RuntimeException e) {
//
}
}
}, delay, interval); //指定timer在delay时间后开始执行第一次任务,任务之间间隔interval的时长

timer.cancel(); // 关闭当前定时器,根据具体情况使用

注意一开始直接用lambda表达式()->{}会报错,看来不像排序sort方法中的comparator和线程中的runable一样可以直接简写

2.2 通过thread实现定时器

同理,只是写法更复杂些,直接上demo code

//使用thread
Thread thread = new Thread(() -> {
try {
Thread.sleep(delay);
} catch (Exception e) {
e.printStackTrace();
}
while (true) {
try {
scanAndDelete();
Thread.sleep(interval);//注意此处要用类去调用静态方法,直接用实例thread调用会报其可能尚未实例化
} catch (Exception e) {
e.printStackTrace();
}
}
}, "scanThread");
thread.start();
}

2.3 单线程timer的弊端

通过上述两种方式实现的timer都是单线程工作的,会有以下弊端:
1.管理多个延时任务的缺陷
如果存在多个任务,且任务时间过长,超过了两个任务的间隔时间,会导致后续任务被推迟进行(因为单线程还在处理前一个任务)

2.任务抛出异常时的缺陷
如果TimerTask抛出RuntimeException,Timer会停止所有任务的运行(即不仅仅当前任务停止了,后续所有的任务也都不会再运行),因此如果我们要使用timer,必须捕获RuntimeException

3.Timer执行周期任务时依赖系统时间
Timer执行周期任务时依赖系统时间,如果当前系统时间发生变化会出现一些执行上的变化,ScheduledExecutorService基于时间的延迟,不会由于系统时间的改变发生执行变化。

来源:https://blog.csdn.net/lmj623565791/article/details/27109467

2.4 scheduledThreadPool

为了解决上述单线程timer的三个问题,我们可以通过线程池的思想实现,相比上面的版本,其实就是把任务交给了线程池去执行,改进后的代码并不复杂,demo如下:

ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(2);

QueueWithTimeOut(int size, int delay, int interval) {
this.size = size;
this.que = new LinkedList();
this.timer = new Timer();
// timer.schedule(new TimerTask() {
// @Override
// public void run() {
// scanAndDelete();
// }
// }, delay, interval);
scheduledThreadPool.schedule(()->{
scanAndDelete();
},interval, TimeUnit.MILLISECONDS);
}

2.5 Spring timer(推荐)

通过spring实现timer就更简单了,如1.2配置完之后,就可以通过@Scheduled标签与cron表达式实现定时任务,形式如下:

@Scheduled(cron = "0/5 * * * * ?")
public void doSomeThing() {
scanAndDelete();
}

对比一下是最简洁的实现,也不再需要自己去维护timer的生命周期,此外另一个好处是我们可以通过配置文件将其配置成线程池的版本,这样就很好地避免了单线程timer会产生的问题

3.定时器的分布式设计

最后说说timer分布式设计的思路,可能有些业务场景只需要执行一次定时任务,但每一台机器都起了自己的timer,这时候有两种思路去解决:

1.所有的timer正常运行,但要执行前去竞争一个redis的分布式锁,通过setnx保证只有一个定时器能够做这个任务,即每次做任务的可能是不同的定时器,但任务是相同的

2.更为简单的处理是只在一台机器的timer上起这个任务(如果可以的话)

具体取舍还是要看业务场景而定

Author: Apiao
Link: https://Apiao-1.github.io/2019/04/09/2019-04-07/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.