专注Java教育14年 全国咨询/投诉热线:444-1124-454
赢咖4LOGO图
始于2009,口口相传的Java黄埔军校
首页 hot资讯 Spring定时器详解

Spring定时器详解

更新时间:2021-12-10 10:10:58 来源:赢咖4 浏览3399次

Spring定时器是什么?小编来告诉大家。

1. 定时器——基础

Timer和TimerTask是用于在后台线程中调度任务的 java util 类。简而言之 - TimerTask是要执行的任务,Timer是调度程序。

2. 安排一次任务

(1)在给定的延迟之后

让我们首先在Timer的帮助下简单地运行单个任务:

@Test
public void givenUsingTimer_whenSchedulingTaskOnce_thenCorrect() {
    TimerTask task = new TimerTask() {
        public void run() {
            System.out.println("Task performed on: " + new Date() + "n" +
              "Thread's name: " + Thread.currentThread().getName());
        }
    };
    Timer timer = new Timer("Timer");    
    long delay = 1000L;
    timer.schedule(task, delay);
}

现在,这在一定的延迟后执行任务,作为schedule()方法的第二个参数给出 。我们将在下一节看到如何在给定的日期和时间安排任务。

请注意,如果我们正在运行这是一个 JUnit 测试,我们应该添加一个Thread.sleep(delay * 2)调用,以允许 Timer 的线程在 Junit 测试停止执行之前运行任务。

(2)在给定的日期和时间

现在,让我们看看Timer#schedule(TimerTask, Date)方法,它接受一个Date而不是long作为它的第二个参数,允许我们在某个时刻安排任务,而不是在延迟之后。

这一次,让我们假设我们有一个旧的遗留数据库,我们希望将其数据迁移到一个具有更好模式的新数据库中。

我们可以创建一个DatabaseMigrationTask类来处理该迁移:

public class DatabaseMigrationTask extends TimerTask {
    private List<String> oldDatabase;
    private List<String> newDatabase;
    public DatabaseMigrationTask(List<String> oldDatabase, List<String> newDatabase) {
        this.oldDatabase = oldDatabase;
        this.newDatabase = newDatabase;
    }
    @Override
    public void run() {
        newDatabase.addAll(oldDatabase);
    }
}

为简单起见,我们代表着两个数据库通过列表的 字符串。简单地说,我们的迁移包括将第一个列表中的数据放入第二个列表中。

要在所需的时刻执行此迁移,我们必须使用schedule ()方法的重载版本 :

List<String> oldDatabase = Arrays.asList("Harrison Ford", "Carrie Fisher", "Mark Hamill");
List<String> newDatabase = new ArrayList<>();
LocalDateTime twoSecondsLater = LocalDateTime.now().plusSeconds(2);
Date twoSecondsLaterAsDate = Date.from(twoSecondsLater.atZone(ZoneId.systemDefault()).toInstant());
new Timer().schedule(new DatabaseMigrationTask(oldDatabase, newDatabase), twoSecondsLaterAsDate);

如我们所见,我们将迁移任务以及执行日期提供给 schedule()方法。

然后,在twoSecondsLater指示的时间执行迁移:

while (LocalDateTime.now().isBefore(twoSecondsLater)) {
    assertThat(newDatabase).isEmpty();
    Thread.sleep(500);
}
assertThat(newDatabase).containsExactlyElementsOf(oldDatabase);

虽然我们在这一刻之前,迁移不会发生。

3. 安排一个可重复的任务

既然我们已经介绍了如何安排任务的单次执行,让我们看看如何处理可重复的任务。

再一次,Timer 类提供了多种可能性 :我们可以设置重复以观察固定延迟或固定速率。

固定延迟意味着执行将在上次执行开始后的一段时间开始,即使它被延迟(因此本身被延迟)。

假设我们想每两秒安排一次任务,第一次执行需要一秒钟,第二次执行需要两秒钟但延迟一秒钟。然后,第三次执行将在第五秒开始:

0s     1s    2s     3s           5s
|--T1--|
|-----2s-----|--1s--|-----T2-----|
|-----2s-----|--1s--|-----2s-----|--T3--|

另一方面,固定速率意味着每次执行都将遵守初始计划,无论之前的执行是否被延迟。

让我们重用前面的例子,在固定速率下,第二个任务将在三秒后开始(因为延迟)。但是,四秒后的第三个(尊重每两秒执行一次的初始计划):

0s     1s    2s     3s    4s
|--T1--|       
|-----2s-----|--1s--|-----T2-----|
|-----2s-----|-----2s-----|--T3--|

这两个原理都讲完了,让我们看看如何使用它们。

为了使用固定延迟调度,schedule()方法还有两个重载,每个重载都有一个额外的参数,以毫秒为单位说明周期性。

为什么有两个重载?因为仍然有可能在某个时刻或在某个延迟之后开始任务。

至于固定速率调度,我们有两个 scheduleAtFixedRate()方法也以毫秒为单位。同样,我们有一种方法可以在给定的日期和时间启动任务,另一种方法可以在给定的延迟后启动它。

还值得一提的是,如果任务花费的时间比执行周期长,无论我们使用固定延迟还是固定速率,它都会延迟整个执行链。

这两个原理都讲完了,让我们看看如何使用它们。

为了使用固定延迟调度,schedule()方法还有两个重载,每个重载都有一个额外的参数,以毫秒为单位说明周期性。

为什么有两个重载?因为仍然有可能在某个时刻或在某个延迟之后开始任务。

至于固定速率调度,我们有两个 scheduleAtFixedRate()方法也以毫秒为单位。同样,我们有一种方法可以在给定的日期和时间启动任务,另一种方法可以在给定的延迟后启动它。

还值得一提的是,如果任务花费的时间比执行周期长,无论我们使用固定延迟还是固定速率,它都会延迟整个执行链。

(1)固定延迟

现在,让我们假设我们想要实施一个时事通讯系统,每周向我们的关注者发送一封电子邮件。在这种情况下,重复性任务似乎很理想。

所以,让我们每秒安排一次时事通讯,这基本上是垃圾邮件,但由于发送是假的,我们很高兴!

让我们首先设计一个 NewsletterTask:

public class NewsletterTask extends TimerTask {
    @Override
    public void run() {
        System.out.println("Email sent at: " 
          + LocalDateTime.ofInstant(Instant.ofEpochMilli(scheduledExecutionTime()), 
          ZoneId.systemDefault()));
    }
}

每次执行时,任务都会打印其计划时间,我们使用TimerTask#scheduledExecutionTime()方法收集这些时间。

那么,如果我们想在固定延迟模式下每秒调度这个任务怎么办?我们将不得不使用我们之前讨论过的schedule()的重载版本:

new Timer().schedule(new NewsletterTask(), 0, 1000);
for (int i = 0; i < 3; i++) {
    Thread.sleep(1000);
}

当然,我们只对少数情况进行测试:

Email sent at: 2020-01-01T10:50:30.860
Email sent at: 2020-01-01T10:50:31.860
Email sent at: 2020-01-01T10:50:32.861
Email sent at: 2020-01-01T10:50:33.861

正如我们所看到的,每次执行之间至少有 1 秒的时间,但它们有时会延迟一毫秒。这种现象是由于我们决定使用固定延迟重复。

(2)固定利率

现在,如果我们使用固定速率重复呢?然后我们将不得不使用 scheduleAtFixedRate()方法:

new Timer().scheduleAtFixedRate(new NewsletterTask(), 0, 1000);
for (int i = 0; i < 3; i++) {
    Thread.sleep(1000);
}

这一次,执行不会被前面的延迟:

Email sent at: 2020-01-01T10:55:03.805
Email sent at: 2020-01-01T10:55:04.805
Email sent at: 2020-01-01T10:55:05.805
Email sent at: 2020-01-01T10:55:06.805

(3)安排每日任务

接下来,让我们每天运行一次任务:

@Test
public void givenUsingTimer_whenSchedulingDailyTask_thenCorrect() {
    TimerTask repeatedTask = new TimerTask() {
        public void run() {
            System.out.println("Task performed on " + new Date());
        }
    };
    Timer timer = new Timer("Timer");    
    long delay = 1000L;
    long period = 1000L * 60L * 60L * 24L;
    timer.scheduleAtFixedRate(repeatedTask, delay, period);
}

4.取消定时器和TimerTask

可以通过以下几种方式取消任务的执行:

(1)在运行中取消TimerTask

通过在TimerTask本身的run()方法实现中调用TimerTask.cancel()方法:

@Test
public void givenUsingTimer_whenCancelingTimerTask_thenCorrect()
  throws InterruptedException {
    TimerTask task = new TimerTask() {
        public void run() {
            System.out.println("Task performed on " + new Date());
            cancel();
        }
    };
    Timer timer = new Timer("Timer");    
    timer.scheduleAtFixedRate(task, 1000L, 1000L);    
    Thread.sleep(1000L * 2);
}

(2)取消定时器

通过在Timer对象上调用Timer.cancel()方法:

@Test
public void givenUsingTimer_whenCancelingTimer_thenCorrect() 
  throws InterruptedException {
    TimerTask task = new TimerTask() {
        public void run() {
            System.out.println("Task performed on " + new Date());
        }
    };
    Timer timer = new Timer("Timer");    
    timer.scheduleAtFixedRate(task, 1000L, 1000L);    
    Thread.sleep(1000L * 2); 
    timer.cancel(); 
}

(3)停止内部运行的TimerTask的线程

您还可以在任务的run方法中停止线程,从而取消整个任务:

@Test
public void givenUsingTimer_whenStoppingThread_thenTimerTaskIsCancelled() 
  throws InterruptedException {
    TimerTask task = new TimerTask() {
        public void run() {
            System.out.println("Task performed on " + new Date());
            // TODO: stop the thread here
        }
    };
    Timer timer = new Timer("Timer");
    timer.scheduleAtFixedRate(task, 1000L, 1000L);    
    Thread.sleep(1000L * 2); 
}

注意run实现中的 TODO 指令——为了运行这个简单的例子,我们需要实际停止线程。

在现实世界的自定义线程实现中,应该支持停止线程,但在这种情况下,我们可以忽略弃用并在 Thread 类本身上使用简单的停止API。

5.定时器vs ExecutorService

你也可以很好地利用一个ExecutorService来调度定时器任务,而不是使用定时器。

以下是如何以指定的时间间隔运行重复任务的快速示例:

@Test
public void givenUsingExecutorService_whenSchedulingRepeatedTask_thenCorrect() 
  throws InterruptedException {
    TimerTask repeatedTask = new TimerTask() {
        public void run() {
            System.out.println("Task performed on " + new Date());
        }
    };
    ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
    long delay  = 1000L;
    long period = 1000L;
    executor.scheduleAtFixedRate(repeatedTask, delay, period, TimeUnit.MILLISECONDS);
    Thread.sleep(delay + period * 3);
    executor.shutdown();
}

那么Timer和ExecutorService解决方案的主要区别是什么:

定时器可以对系统时钟的变化敏感;ScheduledThreadPoolExecutor不是

定时器只有一个执行线程;ScheduledThreadPoolExecutor可以配置任意数量的线程

TimerTask 中抛出的运行时异常会杀死线程,因此后续的计划任务不会继续运行;with ScheduledThreadExecutor - 当前任务将被取消,但其余的将继续运行

6.结论

本教程说明了可以利用Java 中内置的简单而灵活的Timer和TimerTask基础结构来快速安排任务的多种方法。当然,如果您需要,Java 世界中还有更复杂和完整的解决方案——例如Quartz 库——但这是一个非常好的起点。

这些示例的实现可以在GitHub项目中找到——这是一个基于 Eclipse 的项目,因此应该很容易导入和运行。

提交申请后,顾问老师会电话与您沟通安排学习

免费课程推荐 >>
技术文档推荐 >>