Java Timer

1. Introduction

Timer and TimerTask are java util classes used to schedule tasks in background threads. In short, TimerTask is the task to be executed, and timer is the scheduler.

2. Schedule one-time tasks

2.1 execution after specified delay

Let's start by simply running a single task with the help of a 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);
}

The delay time is given as the second parameter of the schedule () method. We'll learn how to perform tasks on a given date and time in the next section.

Note that if we are running a JUnit test, we should add a thread Sleep (delay*2) call to allow the thread of the timer to run the task before the JUnit test stops executing.

2.2 designated time

Now, let's look at the Timer#schedule (TimerTask, Date) method, which takes the Date instead of long as its second parameter, which enables the task to be executed at a certain time rather than after a delay.

This time, let's assume that we have an old legacy database, and we want to migrate its data to a new database with a better schema. We can create a DatabaseMigrationTask class to handle the migration:

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 use a list of strings to represent the two databases. Simply put, our migration is to put the data in the first list into the second list. To perform this migration at the desired time, we must use an overloaded version of the schedule () method:

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);

We assign the migration task and execution date to the schedule () method. Then, perform the migration at the time indicated by twoSecondsLater:

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

Although we were before this moment, the migration did not happen.

3. Schedule a repeatable task

Now that we've discussed how to schedule a single execution of a task, let's look at how to deal with repeatable tasks. Similarly, the Timer class provides a variety of possibilities: we can set repetition to observe a fixed delay or a fixed frequency.

  • Fixed delay: it means that execution will start within a period of time after the last execution starts, even if it is delayed (so it itself is delayed). Suppose we want to schedule a task every two seconds. The first execution takes one second and the second execution takes two seconds, but it is delayed by one second. Then, the third execution will start from the fifth second:

  • Fixed frequency: it means that each execution will comply with the initial plan, regardless of whether the previous execution is delayed or not. Let's reuse the previous example. Using a fixed frequency, the second task will start in 3 seconds (because of the delay). However, the third execution after four seconds (about the initial plan to execute every two seconds):

About these two scheduling methods, let's see how to use them:

In order to use fixed delay scheduling, the schedule () method also has two overloads, each of which uses an additional parameter to represent the periodicity in milliseconds. Why two reloads? Because it is still possible to start the task at a certain time or after a delay.

As for fixed frequency scheduling, we have two scheduleAtFixedRate () methods, and their cycles are also in milliseconds. Similarly, we have a way to start a task on a given date and time, and another way to start a task after a given delay.

Note: if the execution time of a task exceeds the execution cycle, whether we use a fixed delay or a fixed rate, it will delay the whole execution chain.

3.1 fixed delay

Now, let's imagine that we want to implement a communication system that sends an email to our followers every week. In this case, repetitive tasks seem to be ideal. So, let's arrange communication every second. This is basically spam, but since the sending is fake, don't care:)

Let's first design a task:

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

Each time the task executes, its scheduling time will be printed. We use the TimerTask#scheduledExecutionTime() method to collect these times. So what if we want to schedule this task every second in fixed delay mode? We must use the overloaded version of schedule () mentioned earlier:

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

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

Of course, we only test a few cases:

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 shown above, there is an interval of at least one second between each execution, but sometimes it is delayed by one millisecond. This phenomenon is due to our decision to use fixed delay repetition.

3.3 scheduling a daily task

@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 scheduler and task

4.1 remove the scheduling task from the Run method

In the run () method, call TimerTask. in the implementation of TimerTask itself. Cancel() method:

@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 timer

Call timer Cancel() method:

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

5.Timer vs ExecutorService

We can also use ExecutorService to schedule timer tasks instead of using timers. The following is a quick example of running a repeating 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();
}

What are the main differences between timer and ExecutorService solutions

  • The timer is sensitive to the change of system clock; ScheduledThreadPoolExecutor does not.
  • The timer has only one execution thread; ScheduledThreadPoolExecutor can configure any number of threads.
  • The runtime exception thrown in TimerTask will kill the thread, so the subsequent scheduled tasks will not continue to run; Use ScheduledThreadExecutor – the current task will be canceled, but the remaining tasks will continue to run.

6. Code of this article

Java timer sample code

Added by blintas on Fri, 28 Jan 2022 10:36:50 +0200