Guide to java.util.Timer

Schedulers, in Java and other languages, allow you to:

  • Schedule a task to run once after a certain delay (one-shot)
  • Schedule a task to run multiple times at a given frequency (recurrent)

In Java, there are two classes on the standard library to achieve this: java.util.Timer and java.util.concurrent.ScheduledExecutorService.
Some third-party libraries implement the functionality as well. Its common that “concurrency libraries” have their own implementations that integrate better with their framework. For example the akka library/toolkit exposes their own scheduler, specially designed to integrate better with their actor model.

This article focuses on how to use the Timer class. If you are interested in understanding how it works internally, and learn the fundamentals that enable you to build one from scratch, register below.

Overview

Broadly speaking, the life-cycle of any scheduler looks like:

  1. Creation of the scheduler
  2. Scheduling of tasks, both one-shot and recurrent.
  3. Cancellation of tasks (optional)
  4. Shutdown of the scheduler

You create an instance. That instance is meant to be shared across multiple components of your application. It is thread safe and can handle thousands of tasks. Then you schedule various tasks during the life-cycle of your application, some of which you might need to cancel later on. When your application terminates, or when you otherwise don’t need to schedule more tasks, you shutdown the scheduler not to waste resources.

There are two classes you need to know: Timer and TimerTask.

Create an instance

import java.util.Timer;

Timer timer = new Timer();

You can optionally specify either the thread name, if the thread is a daemon thread, or both:

import java.util.Timer;

Timer timer = new Timer("thread-foo-bar", true);

By default the thread is non-daemon. Therefore, an instance of Timer will prevent the JVM from exiting, even if the timer has no tasks scheduled. You must call timer.cancel() to overcome this.

The constructor of Timer creates one, and exactly one, thread during instantiation. You don’t have direct access to it, but behind the scenes, it is responsible for both scheduling the tasks submitted, and running them. If you were to create 5 Timer objects, you would have a background for each.

As with the executor version, an instance of Timer is thread-safe. You can share it all across your application, with different threads submitting tasks at the same time.

Unlike ScheduledExecutorService, which is an interface, Timer is a concrete class. This is a disadvantage, as relaying on interfaces makes the code cleaner, more composable, and easier to test.

Scheduling

Timer accepts tasks of type TimerTask. This class was designed specifically for Timer, and is not used by anything else. It is an abstract class that your own task types must extend.
It would be better if we could instead provide pure Runnable instances, as these are more common in the java community. This is what ScheduledExecutorService accepts.
The inner workings of TimerTask are quite simple. But you don’t actually need to understand them. You only need to specify the run method:

import java.util.TimerTask;

public class BackupDB extends TimerTask {

  @Override
  public void run() {
    System.out.println("Starting backup");
    try {
      Thread.sleep(10_000);
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
    System.out.println("End backup");
  }
}

⚠️ Important

Tasks should complete quickly! Otherwise they will hog the background thread and delay the remaining tasks. See Delays section.

TimerTask is abstract and implements Runnable. It contains some extra fields and logic related with the state of the task (VIRGIN, SCHEDULED, EXECUTED, CANCELLED) that you don’t need to know about to use. Also, it contains the means to cancel a (timer) task in a thread-safe manner.

The reason why TimerTask has this extra state, is because – as we will see – task cancellation is done via the TimerTask object submitted.

Cancelling a task

As we will see, task cancellation – unlike the ScheduledExecutorService – is done directly on the object of TimerTask submitted.

Another API difference with ScheduledExecutorService is that TimerTask instances are not reusable. This means that if you submit the task more than once you get a run-time error.

TimerTask backupDB = new BackupDB();

timer.schedule(backupDB, 0L);
timer.schedule(backupDB, 2_000L);

leads to:

Exception in thread "main" java.lang.IllegalStateException: Task already scheduled or cancelled
	at java.base/java.util.Timer.sched(Timer.java:413)
	at java.base/java.util.Timer.schedule(Timer.java:205)
	at schedulers.timer.TimerRunner.main(TimerRunner.java:14)
0 s - Start database
3 s - End database

This makes sense. Otherwise, since cancellation is done via TimerTask, what would happen if you cancel a task object that was submitted multiple times?

“single shot” tasks

TimerTask backupDB = new BackupDB();

timer.schedule(backupDB, 3_600_000);  // 3_600_000 millis equals 1 hour

This will run the backup task 1 hour after its submission.
The method schedule returns void. If the task needs to be cancelled, this is done via the object itself:

boolean wasCancelled = backupDB.cancel();

Recurrent tasks

Things are similar if you want the task to run multiple times. You create a TimerTask and then submit it. However, on top of the delay argument, you must specify the period at which the task is intended to run. There are two approaches associated with two different API methods:

Fixed-Delay

// timer.schedule(timer-task, delay, period);
timer.schedule(backupDB, 0, 15_000L); 

Fixed-Rate

// timer.scheduleAtFixedRate(timer-task, delay, period);
timer.scheduleAtFixedRate(backupDB, 0, 15_000L); 

These are very similar, and most times they will behave the same way. The difference is in how they approach delays (I don’t mean the initial delay) to their original plan. Fixed-Delay strives for “smoothness”, Fixed-Rate wants to be as accurate as possible.


For Fixed-Delay, if one iteration of the scheduled task is delayed, the subsequent iterations will take this into account and also be delayed. That is, they will start later than originally specified. They will respect the period between iterations. They maintain “smoothness”. An example where this is useful, is a task for the blinking of the cursor. If one blink is delayed, you still want to maintain a regular interval thereafter.


For Fixed-Rate, if one iteration of the scheduled task is delayed, the subsequent iterations will try to catch-up. So if one task is delayed, the subsequent tasks might ran at a much faster rate (i.e. much smaller intervals), so that the task runs as close to the original plan as possible. An example where you would want this, is a countdown clock that is decreased every second. If one iteration is delayed, then you want the subsequent iterations to catch up asap, and not be delayed.

But what are these delays?

Delays

Tasks, like every other piece of code that runs, must do so on a particular thread. Threads are the smallest unit of execution.

Every time an instance of Timer is created, in the background, a thread is also created that will loop infinitely. On every iteration it picks up the next task to run (if any). All tasks submitted to a Timer will invariably run on this thread.

A thread can only be running one “thing” at a time, and Timer only has one. This logically leads to the possibility that tasks can delay each other. This will happen depending on how long tasks run for, and when tasks are scheduled for.

The following example and diagram highlights the interplay between delays and task scheduling.

Below, Task-A completes very quickly. Imagine it is a heartbeat message between two TCP clients and it is scheduled to run every 5 seconds. You expect heartbeats to be sent at timestamps 0s, 5s, 10s, 15s, 20s, 25s, and so forth.

QuickTask
import java.util.TimerTask;

public class QuickTask extends TimerTask {

  private final long startReference;
  private int iterations = 0;

  private final String taskName;

  public QuickTask(String taskName) {
    this.startReference = System.currentTimeMillis();
    this.taskName = taskName;
  }

  private int lapsedS() {
    return (int) ((System.currentTimeMillis() - startReference) / 1000L);
  }

  @Override
  public void run() {
    ++iterations;
    System.out.printf("%d s - Run %s#%d\n", lapsedS(), taskName, iterations);
  }
}
LongTask

import java.util.TimerTask;

public class LongTask extends TimerTask {

  private final long startReference;
  private final int sleepFor;

  private final String taskName;

  public LongTask(int sleepFor, String taskName) {
    this.startReference = System.currentTimeMillis();
    this.sleepFor = sleepFor;
    this.taskName = taskName;
  }

  private int lapsedS() {
    return (int) ((System.currentTimeMillis() - startReference) / 1000L);
  }

  @Override
  public void run() {
    System.out.printf("%d s - Start %s\n", lapsedS(), taskName);
    try {
      Thread.sleep(sleepFor * 1000L);
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
    System.out.printf("%d s - End %s\n", lapsedS(), taskName);
  }
}

Timer timer = new Timer("concurrency-deep-dives-timer-thread");

TimerTask taskA = new QuickTask("A");
TimerTask taskB = new LongTask(13, "B");

timer.schedule(taskB, 3_000L);
timer.scheduleAtFixedRate(taskA, 0L, 5_000L);

However, some other client schedules “single-shot” Task-B to run at timestamp 3s. Imagine this is a database backup taking 13 seconds to complete.
The scheduler will start by running the first iteration of Task-A. After this completes, the next task to run is Task-B. From 3s to 16s, the thread is busy running this task, which means that 2nd, 3rd and 4th iterations of Task-A cannot possibly run on the desired timeframe of 5, 10, and 15 seconds respectively.

Illustration to show the difference between fixed-delay and fixed-rate recurrent task scheduling.

So when do they run?
It depends on the behavior we want. If its important that tasks run as close to their original time-frame as possible, then you should use fixed-rate. This way the iterations will catch-up as soon as possible. This requires that there will sometimes be a burst of executions. On the short term, they will not respect the specified period of 5 seconds. Notice in the diagram, that the 5th, 6th, and 7th “iterations” of Task-A ran when they were expected. This was only possible because the previous ones ran much faster (smaller periods) than intended.

But this behavior is not appropriate for all types of tasks. If instead you want the time interval to be constant between “iterations”, then you must accept the fact that after a delay occurs, that delay will be propagated downstream, hence the name fixed-delay. Notice on the diagram that this case looks much smoother. The price payed is that now all iterations are delayed by 11 seconds.

Note

In contrast, Java’s more modern scheduler, the ScheduledExecutorService, can be configured with a particular number of threads. The problem is alleviated, though not completely solved.

Cancel a task

Task cancellation is done via the (task) object submitted:

TimerTask backupDB = new BackupDB();

timer.schedule(backupDB, 3_600_000); 
//other code ....
boolean wasCancelled = backupDB.cancel();

The returned boolean is true if something was cancelled. If the task was “one-shot”, then returns true if the cancellation occurs before it started. If the task is “recurring” it returns always true, as it will be preventing future iterations from running.

If you lose the reference to the task, you cannot cancel it later:

timer.schedule(new BackupDB(), 3_600_000); 
// how to cancel ?

Shutdown

Termination of a scheduler generally does 3 things:

  1. Prevents submission of new tasks (attempting throws)
  2. Discards any tasks already submitted, but which have not yet run.
  3. Terminates threads used to run tasks (invisible to you)

For Timer this is straightforward:

timer.cancel();

The method returns void.
When would you want to do this? The obvious case is when you are shutting down the entire application. If you don’t shutdown the Timer, then if it was created with the default of a non-daemon thread, the instance will prevent the JVM from exiting. When you do timer.cancel(), the worker thread will exit gracefully.
Bear in mind also, that when you do this:

  • The Timer instance is no longer usable. Scheduling new tasks will throw an exception.
  • Tasks already scheduled but that have not yet run will be discarded.
  • If a task is running, then it will finish running.
  • If a recurrent task is running, the current iteration will finish running, and subsequent iterations will not run.

Blowing-up the worker thread

A disadvantage of Timer is that if one task throws, the worker thread will catch the exception BUT it will then exit. The Timer then behaves as if it had been cancelled: No more submissions allowed, and all remaining tasks discarded.

This was a design decision not repeated on ScheduledExecutorService. In practice it means that one “client” can, either intentionally or accidentally, prevent the tasks from other “clients” from running.

import java.util.Timer;
import java.util.TimerTask;

Timer timer = new Timer("concurrency-deep-dives-timer-thread");

TimerTask taskA = new BackupDB2(3, "database");
TimerTask taskB = new TaskThrows(1, "foo-bar");

timer.scheduleAtFixedRate(taskA, 0, 4_000L);
timer.schedule(taskB, 2_000L);
Thread.sleep(5_000L);
System.out.println("Scheduling another ....");
timer.schedule(taskA, 4_000L);

The example above would output:

0 s - Start database
3 s - End database
3 s - Start foo-bar
Exception in thread "concurrency-deep-dives-timer-thread" java.lang.RuntimeException: Throwing for task foo-bar on thread concurrency-deep-dives-timer-thread after 1 seconds
	at schedulers.timer.TaskThrows.run(TaskThrows.java:31)
	at java.base/java.util.TimerThread.mainLoop(Timer.java:566)
	at java.base/java.util.TimerThread.run(Timer.java:516)
Scheduling another ....
Exception in thread "main" java.lang.IllegalStateException: Timer already cancelled.
	at java.base/java.util.Timer.sched(Timer.java:409)
	at java.base/java.util.Timer.schedule(Timer.java:205)
	at schedulers.timer.TimerRunner.main(TimerRunner.java:17)