Proper shutdown of a ScheduledExecutorService

ScheduledExecutorService is a Java interface that implements ExecutorService and allows for scheduling of periodic jobs by providing two methods:

  • scheduledAtFixedRate, which allows us to run a job periodically, after an initialDelay and for a provided period of time.
  • scheduleWithFixedDelay, which allows us to run a job again periodically, only this time it waits for the job to first terminate before starting it again after a fixed delay.

An interesting detail came up while I was adapting the example from the official JavaDocs  which has to do with shutting down the ScheduledExecutorService. We always need to ensure a way of shutting down ExecutorService instances (in an orderly fashion... or not), otherwise the running thread cannot be certain that future jobs won't be submitted to the service and it will remain idle.

So let’s have a look at an adapted BeeperControl example, where the beeper beeps every 3 seconds after an initial delay of 2 seconds. The example is single-threaded, since we are interested not in the multi-threaded abilities of an ExecutorService in this example, but only in the scheduling capabilities of ScheduledExecutorService.

public class BeeperControl {

    public static void main(String[] args) {
        ScheduledExecutorService scheduler =   Executors.newSingleThreadScheduledExecutor();
        Runnable beeper = () -> System.out.println("beep");
        ScheduledFuture<?> beeperHandle = scheduler.scheduleAtFixedRate(beeper, 2, 3, SECONDS);
        Runnable canceller = () -> {
            beeperHandle.cancel(false);
        };
        scheduler.schedule(canceller, 30, SECONDS);
        scheduler.shutdown();
    }
}

beeperHandle is a ScheduledFuture instance which will allow us to cancel the computation (or at least attempt to cancel the computation) without evaluating its result just yet. It can also be used to query the computation about being done, cancelled and, of course, to retrieve its result. In this example, we define a Runnable that uses the ScheduledFuture beeperHandle to cancel the scheduled job after 30 seconds. We are thus expecting that we will see 10 “beep”s in our output.

However, if we were to run this (e.g, in IntelliJ), we note that there is no output at all, as if the jobs are never really executed:

Our main function does not produce any output, but at least the main thread dies because of our call to shutdown().

So what’s going on here?

It turns out that the problem is that while we have deferred executions of both our beeper and our canceller into the future, we are calling shutdown() on our ScheduledExecutorService before those jobs have an opportunity to run! After the initial delay is passed, those jobs are passed on to the ExecutorService sub-object through it’s submit(Runnable r) method, which will unfortunately fail since our main thread has already called shutdown()! From the official JavaDocs for shutdown():


Initiates an orderly shutdown in which previously submitted tasks are executed, but no new tasks will be accepted. Invocation has no additional effect if already shut down […]


Consequently, we need to also defer the shutdown() until the computation itself has been completed! To do this, we simply need to move the call to shutdown() within the Runnable that is scheduled for 30 seconds down the line:

public class BeeperControl {

    public static void main(String[] args) {
        ScheduledExecutorService scheduler =   Executors.newSingleThreadScheduledExecutor();
        Runnable beeper = () -> System.out.println("beep");
        ScheduledFuture<?> beeperHandle = scheduler.scheduleAtFixedRate(beeper, 2, 3, SECONDS);
        Runnable canceller = () -> {
            beeperHandle.cancel(false);
            scheduler.shutdown(); // <---- Now the call is within the `canceller` Runnable.
        };
        scheduler.schedule(canceller, 30, SECONDS);
    }
}

This tweak enables our ScheduledExecutorService to submit the jobs when it’s time without being blocked by a call to shutdown() from the main thread, yielding the desired results:

After moving the shutdown into the canceller Runnable, everything runs as expected!

Further reading on ExecutorService, especially the invokeAll() methods, is recommended.

Previous
Previous

When formulaic testing becomes counter-productive

Next
Next

Emulating ExpectedException with the command pattern