Java Timer – Java 定时器

最后修改: 2014年 11月 2日

中文/混合/英文(键盘快捷键:t)

1. Timer – the Basics

1.计时器–基础知识

Timer and TimerTask are java util classes that we use to schedule tasks in a background thread. Basically, TimerTask is the task to perform, and Timer is the scheduler.

TimerTimerTask是java的利用类,我们用它来安排后台线程中的任务。基本上,TimerTask是要执行的任务,而Timer是调度器

2. Schedule a Task Once

2.安排一次任务

2.1. After a Given Delay

2.1.在一个给定的延迟后

Let’s start by simply running a single task with the help of a Timer:

让我们从简单的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);
}

This performs the task after a certain delay, which we gave as the second parameter of the schedule() method. In the next section, we’ll see how to schedule a task at a given date and time.

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

Note that if we’re running this as a JUnit test, we should add a Thread.sleep(delay * 2) call to allow the Timer’s thread to run the task before the Junit test stops executing.

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

2.2. At a Given Date and Time

2.2.在一个特定的日期和时间

Now let’s look at the Timer#schedule(TimerTask, Date) method, which takes a Date instead of a long as its second parameter. This allows us to schedule the task at a certain instant, rather than after a delay.

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

This time, let’s imagine we have an old legacy database, and we want to migrate its data into a new database with a better schema.

这一次,让我们想象一下,我们有一个旧的遗留数据库,我们想把它的数据迁移到一个具有更好模式的新数据库中。

We can create a DatabaseMigrationTask class that will handle this migration:

我们可以创建一个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);
    }
}

For simplicity, we’re representing the two databases by a List of String. Simply put, our migration consists of putting the data from the first list into the second.

为了简单起见,我们用一个ListString来表示这两个数据库。简单地说,我们的迁移包括把第一个列表中的数据放到第二个列表中。

To perform this migration at the desired instant, we’ll have to use the overloaded version of the schedule() method:

为了在所需的瞬间执行这种迁移,我们必须使用重载版的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);

As we can see, we give the migration task, as well as the date of execution to the schedule() method.

正如我们所看到的,我们给了迁移任务,以及执行日期给schedule()方法。

Then the migration is executed at the time indicated by twoSecondsLater:

然后在twoSecondsLater所示的时间执行迁移。

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

Before that moment, the migration doesn’t occur.

在那一刻之前,迁移不会发生。

3. Schedule a Repeatable a Task

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

Now that we’ve covered how to schedule the single execution of a task, let’s see how to deal with repeatable tasks.

现在我们已经介绍了如何安排一个任务的单一执行,让我们看看如何处理可重复的任务。

Once again, the Timer class offers multiple possibilities. We can set up the repetition to observe either a fixed delay or a fixed rate.

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

A fixed delay means that the execution will start a period of time after the moment the last execution started, even if it was delayed (therefore being itself delayed).

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

Let’s say we want to schedule a task every two seconds, with the first execution taking one second and the second one taking two, but being delayed by one second. Then the third execution starts at the fifth second:

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

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

On the other hand, a fixed rate means that each execution will respect the initial schedule, no matter if a previous execution has been delayed.

另一方面,固定费率意味着每次执行都将遵守最初的时间表,无论之前的执行是否被推迟

Let’s reuse our previous example. With a fixed rate, the second task will start after three seconds (because of the delay), but the third one will start after four seconds (respecting the initial schedule of one execution every two seconds):

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

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

Now that we’ve covered these two principles, let’s see how we can use them.

现在我们已经涵盖了这两个原则,让我们看看我们如何使用它们。

In order to use fixed-delay scheduling, there are two more overloads of the schedule() method, each taking an extra parameter stating the periodicity in milliseconds.

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

Why two overloads? Because there’s still the possibility to start the task at a certain moment or after a certain delay.

为什么有两个过载?因为仍有可能在某一时刻或某一延迟后启动任务。

As for the fixed-rate scheduling, we have the two scheduleAtFixedRate() methods, which also take the periodicity in milliseconds. Again, we have one method to start the task at a given date and time, and another to start it after a given delay.

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

It’s also worth mentioning that, if a task takes more time than the period to execute, it delays the whole chain of executions, whether we’re using fixed-delay or fixed-rate.

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

3.1. With a Fixed Delay

3.1.有固定的延迟

Now let’s imagine we want to implement a newsletter system, sending an email to our followers every week. In this case, a repetitive task seems ideal.

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

So let’s schedule the newsletter every second, which is basically spamming, but as the sending is fake, we’re good to go.

因此,让我们每秒钟安排一次通讯,这基本上是垃圾邮件,但由于发送是假的,我们就可以了。

First, we’ll design a NewsletterTask:

首先,我们将设计一个NewsletterTask

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

Each time it executes, the task will print its scheduled time, which we gather using the TimerTask#scheduledExecutionTime() method.

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

So what if we want to schedule this task every second in fixed-delay mode? We’ll have to use the overloaded version of schedule() that we mentioned earlier:

那么,如果我们想在固定延迟模式下每秒安排这个任务呢?我们必须使用我们前面提到的schedule()的重载版本。

new Timer().schedule(new NewsletterTask(), 0, 1000);

for (int i = 0; i < 3; i++) {
    Thread.sleep(1000);
}

Of course, we only carry the tests for a few occurrences:

当然,我们只进行少数情况下的测试。

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

As we can see, there’s at least one second between each execution, but they’re sometimes delayed by a millisecond. This phenomenon is due to our decision to used fixed-delay repetition.

我们可以看到,每次执行之间至少有一秒钟的间隔,但它们有时会延迟一毫秒。这种现象是由于我们决定使用固定延迟的重复方式。

3.2. With a Fixed Rate

3.2.有固定利率

Now what if we were to use a fixed-rate repetition? Then we would have to use the scheduledAtFixedRate() method:

现在,如果我们要使用固定速率的重复呢?那么我们将不得不使用scheduledAtFixedRate()方法。

new Timer().scheduleAtFixedRate(new NewsletterTask(), 0, 1000);

for (int i = 0; i < 3; i++) {
    Thread.sleep(1000);
}

This time, executions aren’t delayed by the previous ones:

这一次,执行并没有被之前的执行所延迟

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.3. Schedule a Daily Task

3.3.安排一个日常任务

Next, let’s run a task once a day:

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

@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. Cancel Timer and TimerTask

4、取消TimerTimerTask

The execution of a task can be canceled in a few ways.

一个任务的执行可以通过几种方式取消。

4.1. Cancel the TimerTask Inside Run

4.1.取消TimerTask里面的Run

The first option is to call the TimerTask.cancel() method inside the run() method’s implementation of the TimerTask itself:

第一种方法是在run()方法对TimerTask本身的实现中调用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);
}

4.2. Cancel the Timer

4.2.取消Timer

Another option is to call the Timer.cancel() method on a Timer object:

另一个选择是在一个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(); 
}

4.3. Stop the Thread of the TimerTask Inside Run

4.3.在Run内停止TimerTask的线程>

We can also stop the thread inside the run method of the task, thus canceling the entire task:

我们也可以在任务的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); 
}

Notice the TODO instruction in the run implementation; in order to run this simple example, we’ll need to actually stop the thread.

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

In a real-world custom thread implementation, stopping the thread should be supported, but in this case, we can ignore the deprecation and use the simple stop API on the Thread class itself.

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

5. Timer vs ExecutorService

5.Timer vs ExecutorService

We can also make good use of an ExecutorService to schedule timer tasks, instead of using the timer.

我们还可以很好地利用ExecutorService来安排定时器任务,而不是使用定时器。

Here’s a quick example of how to run a repeated task at a specified interval:

下面是一个快速的例子,说明如何在一个指定的时间间隔内运行一个重复的任务。

@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();
}

So what are the main differences between the Timer and the ExecutorService solution:

那么,TimerExecutorService解决方案之间的主要区别是什么。

  • Timer can be sensitive to changes in the system clock; ScheduledThreadPoolExecutor isn’t.
  • Timer has only one execution thread; ScheduledThreadPoolExecutor can be configured with any number of threads.
  • Runtime Exceptions thrown inside the TimerTask kill the thread, so the following scheduled tasks won’t run further; with ScheduledThreadExecutor, the current task will be canceled, but the rest will continue to run.

6. Conclusion

6.结论

In this article, we illustrated the many ways we can make use of the simple, yet flexible Timer and TimerTask infrastructure built into Java for quickly scheduling tasks. There are, of course, much more complex and complete solutions in the Java world if we need them, such as the Quartz library, but this is a very good place to start.

在这篇文章中,我们说明了我们可以利用Java中内置的简单而灵活的TimerTimerTask基础设施来快速调度任务的许多方法。当然,如果我们需要,在 Java 世界中还有更复杂、更完整的解决方案,例如 石英库,但这是一个非常好的起点。

The implementation of these examples can be found in over on GitHub.

这些示例的实现可以在GitHub上找到