【股东筹码分析源码】【气泡文章源码】【谷粒教育 源码】schedulethreadpool源码

时间:2024-11-20 07:00:47 编辑:PHP恋爱话术源码 来源:告白网页源码

1.深入理解 RxJava2:Scheduler(2)
2.一起来探究@Schedule定时任务在分布式产生的源码问题
3.可动态配置的Schedule设计
4.Timer和ScheduledThreadPoolExecutor的区别

schedulethreadpool源码

深入理解 RxJava2:Scheduler(2)

       欢迎来到深入理解 RxJava2 系列第二篇,本文基于 RxJava 2.2.0 正式版源码,源码将探讨 Scheduler 与 Worker 的源码概念及其实现原理。

       Scheduler 与 Worker 在 RxJava2 中扮演着至关重要的源码角色,它们是源码线程调度的核心与基石。虽然 Scheduler 的源码股东筹码分析源码作用较为熟悉,但 Worker 的源码概念了解的人可能较少。为何在已有 Scheduler 的源码情况下,还要引入 Worker 的源码概念呢?让我们继续探讨。

       首先,源码Scheduler 的源码核心定义是调度 Runnable,支持立即、源码延时和周期性调用。源码而 Worker 是源码任务的最小单元的载体。在 RxJava2 内部实现中,源码通常一个或多个 Worker 对应一个 ScheduledThreadPoolExecutor 对象,这里暂不深入探讨。

       在 RxJava 1.x 中,Scheduler 没有 scheduleDirect/schedulePeriodicallyDirect 方法,只能先创建 Worker,再通过 Worker 来调度任务。这些方法是对 Worker 调度的简化,可以理解为创建一个只能调度一次任务的 Worker 并立即调度该任务。在 Scheduler 基类的源码中,默认实现是直接创建 Worker 并创建对应的 Task(虽然在部分 Scheduler 的覆盖实现上并没有创建 Worker,但可以认为存在虚拟的 Worker)。

       一个 Scheduler 可以创建多个 Worker,这两者是一对多的关系,而 Worker 与 Task 也是一对多的关系。Worker 的存在旨在确保两件事:统一调度 Runnable 和统一取消任务。例如,在 observeOn 操作符中,可以通过 Worker 来统一调度和取消一系列的 Runnable。

       RxJava2 默认内置了多种 Scheduler 实现,适用于不同场景,这些 Scheduler 都可以在 Schedulers 类中直接获得。以下是两个常用 Scheduler 的源码分析:computation 和 io。

       NewThreadWorker 在 computation、io 和 newThread 中都有涉及,下面简单了解一下这个类。NewThreadWorker 与 ScheduledThreadPoolExecutor 之间是一对一的关系,在构造函数中通过工厂方法创建一个 corePoolSize 为 1 的气泡文章源码 ScheduledThreadPoolExecutor 对象并持有。

       ScheduledThreadPoolExecutor 从 JDK1.5 开始存在,这个类继承于 ThreadPoolExecutor,支持立即、延时和周期性任务。但是注意,在 ScheduledThreadPoolExecutor 中,maximumPoolSize 参数是无效的,corePoolSize 表示最大线程数,且它的队列是无界的。这里不再深入探讨该类,否则会涉及太多内容。

       有了这个类,RxJava2 在实现 Worker 时就站在了巨人的肩膀上,线程调度可以直接使用该类解决,唯一的麻烦之处就是封装一层 Disposable 的逻辑。

       ComputationScheduler 是计算密集型的 Scheduler,其线程数与 CPU 核心数密切相关。当线程数远超过 CPU 核心数目时,CPU 的时间更多地损耗在了线程的上下文切换。因此,保持最大线程数与 CPU 核心数一致是比较通用的方式。

       FixedSchedulerPool 可以看作是固定数量的真正 Worker 的缓存池。确定了 MAX_THREADS 后,在 ComputationScheduler 的构造函数中会创建 FixedSchedulerPool 对象,FixedSchedulerPool 内部会直接创建一个长度为 MAX_THREADS 的 PoolWorker 数组。PoolWorker 继承自 NewThreadWorker,但没有任何额外的代码。

       PoolWorker 的使用方法是从池子里取一个 PoolWorker 并返回。但是需要注意,每个 Worker 是独立的,每个 Worker 内部的任务是绑定在这个 Worker 中的。如果按照上述方法暴露 PoolWorker,会出现两个问题:

       为了解决上述问题,需要在 PoolWorker 外再包一层 EventLoopWorker。EventLoopWorker 是一个代理对象,它会将 Runnable 代理给 FixedSchedulerPool 中取到的 PoolWorker 来调度,并负责管理通过它创建的任务。当自身被取消时,会将创建的任务全部取消。

       与 ComputationScheduler 恰恰相反,IoScheduler 的谷粒教育 源码线程数是无上限的。这是因为 IO 设备的速度远低于 CPU 速度,在等待 IO 操作时,CPU 往往是闲置的。因此,应该创建更多的线程让 CPU 尽可能地利用。当然,并不是线程越多越好,线程数目膨胀到一定程度会影响 CPU 的效率,也会消耗大量的内存。在 IoScheduler 中,每个 Worker 在空置一段时间后就会被清除以控制线程的数目。

       CachedWorkerPool 是一个变长并定期清理的 ThreadWorker 的缓存池,内部通过一个 ConcurrentLinkedQueue 维护。和 PoolWorker 类似,ThreadWorker 也是继承自 NewThreadWorker。仅仅是增加了一个 expirationTime 字段,用来标识这个 ThreadWorker 的超时时间。

       在 CachedWorkerPool 初始化时,会传入 Worker 的超时时间,目前是写死的 秒。这个超时时间表示 ThreadWorker 闲置后最大存活时间(实际中不保证 秒时被回收)。

       IoScheduler 中也存在一个 EventLoopWorker 类,它和 ComputationScheduler 中的作用类似。因为 CachedWorkerPool 是每隔 秒清理一次队列的,所以 ThreadWorker 的存活时间取决于入队的时机。如果一直没有被再次取出,其被实际清理的延迟在 - 秒之间。

       熟悉线程的读者会发现,ComputationScheduler 与 IoScheduler 很像某些参数下的 ThreadPoolExecutor。它们对线程的控制外在表现很相似,但实际的线程执行对象不一样。这两者的对比有助于我们更深刻地理解 Scheduler 设计的内在逻辑。

       Scheduler 是 RxJava 线程的核心概念,RxJava 基于此屏蔽了 Thread 相关的概念,只与 Scheduler/Worker/Runnable 打交道。

       本来计划继续基于 Scheduler 和大家一起探讨 subscribeOn 与 observeOn,但考虑到篇幅问题,这些留待下篇分享。

       感谢大家的阅读,欢迎关注笔者的公众号,可以第一时间获取更新,perl 源码 解密同时欢迎留言沟通。

一起来探究@Schedule定时任务在分布式产生的问题

       探究@Schedule定时任务在分布式中的问题,本文主要讨论了SpringBoot中@EnableScheduling和@Scheduled实现定时任务的方式及其在分布式调度中可能出现的挑战。我们将通过搭建基本环境、分析问题、解释根本原因以及提供解决策略,来解答这一问题。

       一、搭建基本环境

       我们首先引入基本依赖,创建启动类并编写定时任务。为了确保每一步清晰可见,我们将代码示例贴出,方便读者理解。

       二、问题:执行时间延迟和单线程执行

       按照给定的cron表达式@Scheduled(cron = "0/5 * * * * ? "),定时任务每五秒执行一次。然而,实际业务场景中,任务执行时间可能远大于这里的五秒。如果任务执行时间较短,我们可能会忽略延迟性问题。但如果任务执行时间较长,延迟问题将显现。

       我们通过主动让线程睡眠秒,观察输出结果,发现执行结果与预期不符,存在延迟问题。问题根源在于执行任务线程数量不足,即线程阻塞式执行。

       三、为什么会出现上述问题?

       问题的核心在于线程池配置不当。通过启动类上@EnableScheduling注解,Spring自动配置了TaskSchedulingAutoConfiguration类。该类构造了一个线程池,但其核心参数设置为1,导致只有单一线程执行任务。

       在ThreadPoolTaskScheduler中,线程池的核心参数被设置为1。通过查看ThreadPoolTaskScheduler的构建过程,我们发现线程池的构建使用了ScheduledExecutorService的默认参数,其中允许的采货软件源码最大线程数和最大任务等待队列均为Integer.MAX_VALUE。在生产环境中,这样的配置可能导致问题,因为默认线程池的参数设置对高并发和大量任务处理能力不足。

       四、解决方式

       1. 使用@EnableConfigurationProperties(TaskSchedulingProperties.class)配置自定义参数,虽然TaskSchedulingProperties可以配置核心线程数等参数,但无法控制线程池中最大线程数和等待队列数量,这种配置方式存在局限性。

       2. 手动异步编排,将任务交给自定义的线程池执行,这需要开发者自己创建并配置线程池参数。

       3. 异步化定时任务,使用@Async注解,将任务放入自定义线程池执行,确保任务在多线程环境下并行执行,避免延迟问题。

       针对上述方式,我们将实现细节分为修改配置文件、执行逻辑改为异步执行、实现异步定时任务等部分,通过实际代码示例展示解决方案。

       五、分布式下的思考

       在分布式环境下,上述解决方法在不引入第三方库的情况下,能有效应对大部分情况。然而,多个项目同时运行定时任务时,可能存在并发执行问题,导致数据不一致。为了解决并发执行导致的脏数据问题,可以采用分布式锁或使用分布式调度框架。

       使用分布式锁,如Redisson,可以在执行定时任务前获取锁,确保任务的唯一执行。在分布式系统中,还需考虑任务执行失败或宕机的情况,如通过日志监控和异常处理机制进行补救。

       总结,解决定时任务在分布式环境下的问题,关键在于合理配置线程池,采用异步执行策略,并引入分布式锁或使用成熟调度框架,以确保任务的高效、有序执行。

可动态配置的Schedule设计

       1.背景

       定时任务是实际开发中常见的一类功能,例如每天早上凌晨对前一天的注册用户数量、渠道来源进行统计,并以邮件报表的方式发送给相关人员。相信这样的需求,每个开发伙伴都处理过。

       你可以使用Linux的Crontab启动应用程序进行处理,或者直接使用Spring的Schedule对任务进行调度,还可以使用分布式调度系统,如果xxl-job等。相信你已经轻车熟路、习以为常。直到有一天你接到了一个新需求:

       1.新建一组任务,周期性的执行指定SQL并将结果以邮件的方式发送给特定人群;2.比较方便的对任务进行管理,比如启动、停止,修改调度周期等;3.动态添加、移除任务,不需要频繁的修改、发布程序;

       停顿几分钟,简单思考一下,有哪几种实现思路呢?

       本篇文章将从以下几部分进行讨论:

       1.SpringSchedule配置和使用。首先我们将介绍Demo的骨架,并基于Spring-Boot完成Schedule的配置;2.数据库定时轮询方案。使用SpringSchedule定时轮询数据库,并执行相应任务。在执行任务策略中,我们将尝试同步和异步执行两种方案,并对其优缺点进行分析;3.基于TaskScheduler动态配置方案。基于数据库轮询或配置中心两种方案动态的对SpringTaskScheduler进行配置,以实现动态管理任务的目的;4.我们进入分布式环境,利用多个冗余节点解决系统高可用问题,同时使用分布式锁保障只会有一个任务同时执行;

2.SpringSchedule

       SpringBoot上的Schedule的使用非常简单,无需增加新的依赖,只需简单配置即可。

       1.使用@EnableScheduling启用Schedule;2.在要调度的方法上增加@Scheduled;

       首先,我们需要在启动类上添加@EnableScheduling注解,该注解将启用SchedulingConfiguration配置类帮我们完成最基本的配置。

@SpringBootApplication@EnableSchedulingpublicclassConfigurableScheduleDemoApplication{ publicstaticvoidmain(String[]args){ SpringApplication.run(ConfigurableScheduleDemoApplication.class,args);}}

       启用Schedule配置之后,在需要被调度的方法上增加@Scheduled注解。

@ServicepublicclassSpringScheduleService{ @AutowiredprivateTaskServicetaskService;@Scheduled(fixedDelay=5*,initialDelay=)publicvoidrunTask(){ TaskConfigtaskConfig=TaskConfig.builder().name("SpringDefaultSchedule").build();this.taskService.runTask(taskConfig);}}

       runTask任务延迟1s进行初始化,并以5s为间隔进行调度。

       Scheduled注解类的详细配置如下:

配置含义样例cronlinuxcrontab表达式@Scheduled(cron="*/5****MON-FRI")工作日,每5s调度一次fixedDelay固定间隔,上次运行结束,与下次启动运行,相隔固定时长@Scheduled(fixedDelay=)运行结束后,5S后启动一次调度fixedDelayString与fixedDelay一致fixedRate固定周期,前后两次运行相隔固定的时长@Scheduled(fixedRate=)前后两个任务,间隔5秒fixedRateString与fixedRate一致initialDelay第一次执行,间隔时间@Scheduled(initialDelay=,fixedRate=)第一次执行,延时1秒,以后以5秒为周期进行调度initialDelayString与initialDelay一致

       环境搭建完成,让我们开始第一个方案。

3.数据库定时轮询

       使用数据库来管理任务,通过轮询的方案,进行动态调度。首先,我们看下最简单的方案:串行执行方案。

3.1.串行执行方案

       整体思路非常简单,流程如下:

       主要分如下几步:

       1.在应用中启动一个Schedule任务(每1秒调度一次),定时从数据库中获取待执行的任务(状态为可用,下一次执行时间小于当前时间);2.根据数据库的任务配置信息,依次遍历并执行任务;3.任务执行完成后,经过计算获得下一次调度时间,将其写回到数据库;4.等待下一次任务调度。

       核心代码如下:

@Scheduled(fixedDelay=,initialDelay=)publicvoidloadAndRunTask(){ Datenow=newDate();//加载需要运行的任务://1.状态为ENABLE//2.下一次运行时间小于当前时间List<TaskDefinitionV2>shouldRunTasks=loadShouldRunTasks(now);//依次遍历待运行任务,执行对于的任务for(TaskDefinitionV2task:shouldRunTasks){ //DoubleCheckif(task.shouldRun(now)){ //执行任务runTask(task);//更新任务的下一次运行时间updateNextRunTime(task,now);}}}

       方案简单但非常有效,那该方案存在哪些问题呢?最主要的问题就是:任务串行执行,会导致后面任务出现延时运行;同时,下一轮检查也会被delay。

       例如,依次加载了待执行任务task1、task2、task3。其中task1耗时5秒,task2耗时5秒,task3耗时1秒,由于三个任务串行执行,task2将延时5秒,task3延时秒;下一轮检查距上次启动相差秒。

       究其根本,核心问题是调度线程和运行线程是同一个线程,调度的运行和任务的运行相互影响。

       让我们看一个改进方案:并行执行方案。

3.2.并行执行方案

       整体执行流程如下:

       相比之前的方案,新方案引入了线程池,每一个任务对应一个线程池,避免任务间的相互影响;任务在线程池中异步处理,避免了调度线程的延时。具体流程如下:

       1.步骤一不变,在应用中启动一个Schedule任务(每1秒调度一次),定时从数据库中获取待执行的任务(状态为可用,下一次执行时间小于当前时间);2.依次遍历任务,将任务提交到专有线程池中异步执行,调度线程直接返回;3.任务在线程池中运行,结束后更新下一次的运行时间;4.调度线程重新从数据库中获取待执行任务,在将任务提交至线程池中,如果有任务正在执行,使用线程池拒绝策略,抛弃最老的任务;

       核心代码如下:

       Spring调度任务,每1秒运行一次:

@Scheduled(fixedDelay=,initialDelay=)publicvoidloadAndRunTask(){ Datenow=newDate();//加载所有待运行的任务//1.状态为ENABLE//2.下一次运行时间小于当前时间List<TaskDefinitionV2>shouldRunTasks=loadShouldRunTasks(now);//遍历待运行任务for(TaskDefinitionV2task:shouldRunTasks){ //1.根据TaskId获取任务对应的线程池//2.将任务提交至线程池中this.executorServiceForTask(task.getId()).submit(newTaskRunner(task.getId()));}}

       自定义线程池,每个线程池最多只有一个线程,空闲超过秒后,线程自动回收,线程饱和时,直接丢弃最老的任务:

privateExecutorServiceexecutorServiceForTask(LongtaskId){ returnthis.executorServiceRegistry.computeIfAbsent(taskId,id->{ BasicThreadFactorythreadFactory=newBasicThreadFactory.Builder()//指定线程池名称.namingPattern("Async-Task-"+taskId+"-Thread-%d")//设置线程为后台线程.daemon(true).build();//线程池核心配置://1.每个线程池最多只有一个线程//2.线程空闲超过秒进行自动回收//3.直接使用交互器,线程空闲进行任务交互//4.使用指定的线程工厂,设置线性名称//5.线程池饱和,自动丢弃最老的任务returnnewThreadPoolExecutor(0,1,L,TimeUnit.SECONDS,newSynchronousQueue<>(),threadFactory,newThreadPoolExecutor.DiscardOldestPolicy());});}

       最后,在线程池中运行的Task如下:

privateclassTaskRunnerimplementsRunnable{ privatefinalDatenow=newDate();privatefinalLongtaskId;publicTaskRunner(LongtaskId){ this.taskId=taskId;}@Overridepublicvoidrun(){ //重新加载任务,保持最新的任务状态TaskDefinitionV2task=definitionV2Repository.findById(this.taskId).orElse(null);if(task!=null&&task.shouldRun(now)){ //运行任务runTask(task);//更新任务的下一次运行时间updateNextRunTime(task,now);}}}4.TaskScheduler配置方案

       该方案的核心为:绕过@Schedule注解,直接对Spring底层核心类TaskScheduler进行配置。

       TaskScheduler接口是Spring对调度任务的一个抽象,更是@Schedule背后默默的支持者,首先我们看下这个接口定义。

publicinterfaceTaskScheduler{ ScheduledFutureschedule(Runnabletask,Triggertrigger);ScheduledFutureschedule(Runnabletask,InstantstartTime);ScheduledFutureschedule(Runnabletask,DatestartTime);ScheduledFuturescheduleAtFixedRate(Runnabletask,InstantstartTime,Durationperiod);ScheduledFuturescheduleAtFixedRate(Runnabletask,DatestartTime,longperiod);ScheduledFuturescheduleAtFixedRate(Runnabletask,Durationperiod);ScheduledFuturescheduleAtFixedRate(Runnabletask,longperiod);ScheduledFuturescheduleWithFixedDelay(Runnabletask,InstantstartTime,Durationdelay);ScheduledFuturescheduleWithFixedDelay(Runnabletask,DatestartTime,longdelay);ScheduledFuturescheduleWithFixedDelay(Runnabletask,Durationdelay);ScheduledFuturescheduleWithFixedDelay(Runnabletask,longdelay);}

       满满的都是schedule接口,其他的比较简单就不过多叙述了,重点说下Trigger这个接口,首先看下这个接口的定义:

publicinterfaceTrigger{ DatenextExecutionTime(TriggerContexttriggerContext);}

       只有一个方法,获取下次执行的时间。在任务执行完成后,会调用Trigger的nextExecutionTime获取下一次运行时间,从而实现周期性调度。

       CronTrigger是Trigger的最常见实现,以linuxcrontab的方式配置调度任务,如:

scheduler.schedule(task,newCronTrigger("-**MON-FRI"));

       基础部分简单介绍到这,让我们看下数据库动态配置方案。

4.1数据库动态配置方案

       整体设计如下:

       仍旧是轮询数据库方式,详细流程如下:

       1.在应用中启动一个Schedule任务(每1秒调度一次),定时从数据库中获取所有任务;2.依次遍历任务,与内存中的TaskEntry(任务与状态)进行比对,动态的向TaskScheduler中添加或取消调度任务;3.由TaskScheduler负责实际的任务调度;

       核心代码如下:

@Scheduled(fixedDelay=,initialDelay=)publicvoidloadAndConfig(){ //加载所有的任务信息List<TaskDefinitionV3>tasks=repository.findAll();//遍历任务进行任务检查for(TaskDefinitionV3task:tasks){ //获取内存任务状态TaskEntrytaskEntry=this.taskEntry.computeIfAbsent(task.getId(),TaskEntry::new);if(task.isEnable()&&taskEntry.isStop()){ //任务为可用,运行状态为停止,则重新进行schedule注册ScheduledFuture<?>scheduledFuture=this.taskScheduler.scheduleWithFixedDelay(newTaskRunner(task),task.getDelay()*);taskEntry.setScheduledFuture(scheduledFuture);log.info("successtostartscheduletaskfor{ }",task);}elseif(task.isDisable()&&taskEntry.isRunning()){ //任务为禁用,运行状态为运行中,停止正在运行在任务taskEntry.stop();log.info("successtostopscheduletaskfor{ }",task);}}}

       核心辅助类:

@ServicepublicclassSpringScheduleService{ @AutowiredprivateTaskServicetaskService;@Scheduled(fixedDelay=5*,initialDelay=)publicvoidrunTask(){ TaskConfigtaskConfig=TaskConfig.builder().name("SpringDefaultSchedule").build();this.taskService.runTask(taskConfig);}}0

       有没有发现,以上方案都有一个共同的缺陷:基于数据库轮询获取任务,加大了数据库压力。理论上,只有在配置发生变化时才有必要对任务进行更新,接下来让我们看下改进方案:基于配置中心的方案。

4.2配置中心通知方案

       整体设计如下:

       核心流程如下:

       1.应用启动时,从配置中心中获取调度的配置信息,并完成对TaskScheduler的配置;2.当配置发送变化时,配置中心会主动将配置推送到应用程序,应用程序在接收到变化通知时,动态的增加或取消调度任务;3.任务的实际调度仍旧由TaskScheduler完成。

       由于手底下没有配置中心,暂时没有coding,思路很简单,有条件的同学可以自行完成。

5.分布式环境下应用

       以上方案,都是在单机环境下运行,如果应用程序挂掉了,任务调度也就停止了,为了避免这种情况的发生,需要提升系统的可用性,实现冗余部署和自动化容灾。

       以上方案,如果部署多个节点会发生什么?是的,会出现任务被多次调度的问题,为了保障在同一时刻只有一个任务在运行,需要为任务增加一个排他锁。同时,由于排他锁的存在,当一个节点处问题后,另一个节点在调度时会自动获取锁,从而解系统的单点问题。

       为了简单,我们使用Redis的分布式锁。

5.1.环境搭建

       Redisson是Redis的一个富客户端,提供了很多高级的数据结构。本次,我们将使用RLock对应用进行保护。

       首先,在pom中引入RedissonStarter。

@ServicepublicclassSpringScheduleService{ @AutowiredprivateTaskServicetaskService;@Scheduled(fixedDelay=5*,initialDelay=)publicvoidrunTask(){ TaskConfigtaskConfig=TaskConfig.builder().name("SpringDefaultSchedule").build();this.taskService.runTask(taskConfig);}}1

       然后,在application.properties文件中增加Redis配置,具体如下:

@ServicepublicclassSpringScheduleService{ @AutowiredprivateTaskServicetaskService;@Scheduled(fixedDelay=5*,initialDelay=)publicvoidrunTask(){ TaskConfigtaskConfig=TaskConfig.builder().name("SpringDefaultSchedule").build();this.taskService.runTask(taskConfig);}}.2引入分布式锁

       最后,就可以直接使用分布式锁对任务执行进行保护了,代码如下:

@ServicepublicclassSpringScheduleService{ @AutowiredprivateTaskServicetaskService;@Scheduled(fixedDelay=5*,initialDelay=)publicvoidrunTask(){ TaskConfigtaskConfig=TaskConfig.builder().name("SpringDefaultSchedule").build();this.taskService.runTask(taskConfig);}}3

       备注:

       Redis是典型的AP应用,而分布式锁严格意义上来说是CP。所以基于Redis的分布式锁只能使用在非严格环境中,比如我们的数据报表需求。如果设计金钱,需要使用CP实现,如Zookeeper或etcd等。

6.小结

       本文从Spring的Schedule出发,依次对数据库轮询方案、TaskScheduler配置方案进行详细讲解,以实现对调度任务的可配置化。最后,使用Redis分布式锁有效解决了分布式环境下任务重复调度和自动容灾问题。

       仍旧是那句话,架构设计没有更好,只有最适合。同学们可以根据自己的需求自取。

References

       [1]源码:/litao/books/tree/master/configurable-schedule

Timer和ScheduledThreadPoolExecutor的区别

       åœ¨å®žé™…应用中,有时候我们需要创建一些个延迟的、并具有周期性的任务,比如,我们希望当我们的程序启动后每隔1小时就去做一次日志记录。在JDK中提供了两种方法去创建延迟周期性任务。

       Timer

       Timer是java.util包下的一个类,在JDK1.3的时候被引入,Timer只是充当了一个执行者的角色,真正的任务逻辑是通过一个叫做TimerTask的抽象类完成的,TimerTask也是java.util包下面的类,它是一个实现了Runnable接口的抽象类,包含一个抽象方法run( )方法,需要我们自己去提供具体的业务实现。

       Timer类对象是通过其schedule方法执行TimerTask对象中定义的业务逻辑,并且schedule方法拥有多个重载方法提供不同的延迟与周期性服务。

       ä¸‹é¢æ˜¯åˆ©ç”¨Timer去创建的一个延时周期性任务

       import java.text.SimpleDateFormat;

       import java.util.Date;

       import java.util.Timer;

       import java.util.TimerTask;

       public class TestTimer {

        public static void main(String[] args) {

        String time = new SimpleDateFormat("HH:mm:ss").format(new Date());

        System.out.println("Start time : " + time);

        Timer timer = new Timer();

        TimerTask task = new TimerTask() {

        @Override

        public void run() {

        // TODO Auto-generated method stub

        String time = new SimpleDateFormat("HH:mm:ss").format(new Date());

        System.out.println("Now Time : " + time);

        }

        }; //end task

        timer.schedule(task, , );

        }

       }

       ç¨‹åºçš„输出:

       Start time : ::

       Now Time : ::

       Now Time : ::

       Now Time : ::

       Now Time : ::

       ScheduledThreadPoolExecutor

       åœ¨JDK1.5的时候在java.util.concurrent并发包下引入了ScheduledThreadPoolExecutor类,引入它的原因是因为Timer类创建的延迟周期性任务存在一些缺陷, ScheduledThreadPoolExecutor继承了ThreadPoolExecutor,并且实现了ScheduledExecutorService接口, ScheduledThreadPoolExecutor也是通过schedule方法执行Runnable任务的。

       æˆ‘们用 ScheduledThreadPoolExecutor来实现和上述Timer一样的功能

       import java.text.SimpleDateFormat;

       import java.util.Date;

       import java.util.concurrent.ScheduledThreadPoolExecutor;

       import java.util.concurrent.TimeUnit;

       public class TestScheduledThreadPoolExecutor {

        public static void main(String[] args) {

        String time = new SimpleDateFormat("HH:mm:ss").format(new Date());

        System.out.println("Start time : " + time);

        ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(5); //创建5个执行线程

        Runnable runnable = new Runnable() {

        @Override

        public void run() {

        // TODO Auto-generated method stub

        String time = new SimpleDateFormat("HH:mm:ss").format(new Date());

        System.out.println("Now Time : " + time);

        }

        };

        executor.scheduleWithFixedDelay(runnable, 2, 3, TimeUnit.SECONDS);

        }

       }

       ç¨‹åºçš„输出:

       Start time : ::

       Now Time : ::

       Now Time : ::

       Now Time : ::

       Now Time : ::

       è¿™æ ·çœ‹æ¥Timer和 ScheduledThreadPoolExecutor好像没有声明差别,但是 ScheduledThreadPoolExecutor的引入正是由于Timer类存在的一些不足,并且在JDK1.5或更高版本中,几乎没有利用继续使用Timer类,下面说明Timer存在的一些缺点。

       å•çº¿ç¨‹

       Timer类是通过单线程来执行所有的TimerTask任务的,如果一个任务的执行过程非常耗时,将会导致其他任务的时效性出现问题。而 ScheduledThreadPoolExecutor是基于线程池的多线程执行任务,不会存在这样的问题。

       è¿™é‡Œæˆ‘们通过让Timer来执行两个TimerTask任务来说明,其中一个TimerTask的执行过程是耗时的,加入需要2秒。

       import java.text.SimpleDateFormat;

       import java.util.Date;

       import java.util.Timer;

       import java.util.TimerTask;

       public class SingleThreadTimer {

        public static void main(String[] args) {

        String time = new SimpleDateFormat("HH:mm:ss").format(new Date());

        System.out.println("Start time : " + time);

        Timer timer = new Timer();

        TimerTask task1 = new TimerTask() {

        @Override

        public void run() {

        // TODO Auto-generated method stub

        String time = new SimpleDateFormat("HH:mm:ss").format(new Date());

        System.out.println("Task1 time : " + time);

        }

        };

        TimerTask task2 = new TimerTask() {

        @Override

        public void run() {

        // TODO Auto-generated method stub

        try {

        Thread.sleep();

        } catch (InterruptedException e) {

        // TODO Auto-generated catch block

        e.printStackTrace();

        }

        String time = new SimpleDateFormat("HH:mm:ss").format(new Date());

        System.out.println("task2 time : " + time);

        }

        };

        timer.schedule(task1, , );

        timer.schedule(task2, , );

        }

       }

       è¿™é‡Œå®šä¹‰äº†ä¸¤ä¸ªä»»åŠ¡ï¼Œä»»åŠ¡1,程序启动2秒后每隔1秒运行一次,任务2,程序启动2秒后,每隔3秒运行1次,然后让Timer同时运行这两个任务

       ç¨‹åºçš„输出如下:

       Start time : ::

       Task1 time : ::

       task2 time : ::

       Task1 time : ::

       Task1 time : ::

       task2 time : ::

       Task1 time : ::

       Task1 time : ::

       task2 time : ::

       Task1 time : ::

       Task1 time : ::

       å¯ä»¥åˆ†æžï¼Œæ— è®ºæ˜¯ä»»åŠ¡1还是任务2都没有按照我们设定的预期进行运行,造成这个现象的原因就是Timer类是单线程的。

       Timer线程不捕获异常

       Timer类中是不捕获异常的,假如一个TimerTask中抛出未检查异常(P.S: java中异常分为两类:checked exception(检查异常)和unchecked exception(未检查异常),对于未检查异常也叫RuntimeException(运行时异常). ),Timer类将不会处理这个异常而产生无法预料的错误。这样一个任务抛出异常将会导致整个Timer中的任务都被取消,此时已安排但未执行的TimerTask也永远不会执行了,新的任务也不能被调度(所谓的“线程泄漏”现象)。

       ä¸‹é¢å°±å·²å¸¸è§çš„RuntimeException,ArrayIndexOutOfBoundsException数组越界异常,来演示这个缺点:

       import java.text.SimpleDateFormat;

       import java.util.Date;

       import java.util.Timer;

       import java.util.TimerTask;

       public class TestTimerTask {

        public static void main(String[] args) {

        System.out.println(new SimpleDateFormat("HH:mm:ss").format(new Date()));

        Timer timer = new Timer();

        TimerTask task1 = new TimerTask() {

        @Override

        public void run() {

        System.out.println("1: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));

        }

        };

        TimerTask task2 = new TimerTask() {

        @Override

        public void run() {

        int[] arr = { 1,2,3,4,5};

        try {

        Thread.sleep();

        } catch (InterruptedException e) {

        e.printStackTrace();

        }

        int index = (int)(Math.random()*);

        System.out.println(arr[index]);

        System.out.println("2: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));

        }

        };

        timer.schedule(task1, , );

        timer.schedule(task2, , );

        }

       }

       ç¨‹åºä¼šåœ¨è¿è¡Œè¿‡ç¨‹ä¸­æŠ›å‡ºæ•°ç»„越界异常,并且整个程序都会被终止,原来完好的任务1也被终止了。

       åŸºäºŽç»å¯¹æ—¶é—´

       Timer类的调度是基于绝对的时间的,而不是相对的时间,因此Timer类对系统时钟的变化是敏感的,举个例子,加入你希望任务1每个秒执行一次,某个时刻,你将系统时间提前了6秒,那么任务1就会在4秒后执行,而不是秒后。在 ScheduledThreadPoolExecutor,任务的调度是基于相对时间的,原因是它在任务的内部 存储了该任务距离下次调度还需要的时间(使用的是基于 System#nanoTime实现的相对时间 ,不会因为系统时间改变而改变,如距离下次执行还有秒,不会因为将系统时间调前6秒而变成4秒后执行)。

       åŸºäºŽä»¥ä¸Š3个弊端,在JDK1.5或以上版本中,我们几乎没有理由继续使用Timer类,ScheduledThreadPoolExecutor可以很好的去替代Timer类来完成延迟周期性任务。