定时器总结
梳理了下目前用过的定时器实现方案:
- JDK的Timer包
- Thread
- ScheduledThreadPool
- Spring Timer(@Scheduled 推荐)
定时器的启动时间主要有以下:
- 随tomcat启动而启动——原生servlet(落后的方式)
- 随Spring启动而启动
- 其他时间(根据业务逻辑确定)
下面具体介绍每一种的实现及利弊,最后会说明分布式场景下定时器如何正确设置的思路,首先说明启动时机的实现(主要对前两个共性的启动时机进行说明):
1.1原生servlet(落后的方式)
最原始的解决方案,随着tomcat启动而启动,其加载顺序在spring之前,固在spring初始化之前定时器就启动了所以相关注入依赖会报NPE,只能用最原始的直接从上下文里去拿要的变量(不推荐)。
(SpringMVC)在build目录下的web.xml文件中需要添加以下配置(class具体放的是timer的全限定名)
<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 |
1.2 Spring启动后启动
其实从效果而言,Spring启动前启动与启动后再启动并无多大区别,而如果定时器启动时Spring已经启动了,那么就避免了手动去context中获取bean,代码简介多了(我一开始用第一种启动方法纯粹是搜的时候搜错了,第一种其实没有任何的必要,是一种落后的方法,网上的教程什么的完全没必要看,被误导了)
这种方法配置就更简单了,直接找到Spring的配置文件加入相关信息
<!-- 定时任务 --> |
同时注意到此处有一个pool-size
的参数,说明可以配置成线程池,也就没有了单线程timer的不足(后面具体说明)。
下面首先列举经典的jdk的timer使用
2.1JDK的Timer
使用非常简单,直接上代码(以之前蚂蚁二面笔试中的实现一个定时删除过期值得队列中的timer实现为例):
Timer timer = new Timer(); |
注意一开始直接用lambda表达式()->{}会报错,看来不像排序sort方法中的comparator和线程中的runable一样可以直接简写
2.2 通过thread实现定时器
同理,只是写法更复杂些,直接上demo code
//使用thread |
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); |
2.5 Spring timer(推荐)
通过spring实现timer就更简单了,如1.2配置完之后,就可以通过@Scheduled标签与cron表达式实现定时任务,形式如下:
"0/5 * * * * ?") (cron = |
对比一下是最简洁的实现,也不再需要自己去维护timer的生命周期,此外另一个好处是我们可以通过配置文件将其配置成线程池的版本,这样就很好地避免了单线程timer会产生的问题
3.定时器的分布式设计
最后说说timer分布式设计的思路,可能有些业务场景只需要执行一次定时任务,但每一台机器都起了自己的timer,这时候有两种思路去解决:
1.所有的timer正常运行,但要执行前去竞争一个redis的分布式锁,通过setnx保证只有一个定时器能够做这个任务,即每次做任务的可能是不同的定时器,但任务是相同的
2.更为简单的处理是只在一台机器的timer上起这个任务(如果可以的话)
具体取舍还是要看业务场景而定