Task reschedules itself
The Answer by Makato is right about suggesting the use of an executor service. But ending the scheduled executor service as the way to end your repeating task is less than optimal. Often we have one executor service handling many tasks. So while we want our repeating task to end, the executor service may need to continue scheduling other tasks to run.
Instead, ironically, do not schedule the repeating task to repeat. Schedule the task to run once. Have the task check its status.
- If the status requires the task run again, have the task schedule itself on the executor service for another run.
- If the status is complete, and no further executions of that task are needed, simply opt out of another submission to the executor service.
The key to making this work is passing a reference to the scheduled executor service to the constructor of your task. The task keeps that reference as a member field of its own class. So the task can use that reference to pass itself to the scheduled executor service for another submission. The task becomes self-scheduling.
Here is the source code for an example of such a task.
Notice that in addition to passing a reference to the scheduled executor service, we also pass an Instant representing our target moment when we want to stop our repeating task. And we pass a Duration object for the amount of time to wait between executions of this task.
Examine that run method. The task compares the current moment against the target moment. If not yet passed, the task schedules it own next execution. If passed, no rescheduling, mission accomplished.
package work.basil.example.concurrency;
import java.time.Duration;
import java.time.Instant;
import java.util.Objects;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class ExpiringTask implements Runnable
{
private final Instant timeLimit;
private final Duration untilNextRun;
private final ScheduledExecutorService scheduledExecutorService;
public ExpiringTask ( final Instant timeLimit , final Duration untilNextRun , final ScheduledExecutorService scheduledExecutorService )
{
this.timeLimit = Objects.requireNonNull( timeLimit );
this.untilNextRun = Objects.requireNonNull( untilNextRun );
this.scheduledExecutorService = Objects.requireNonNull( scheduledExecutorService );
}
@Override
public void run ( )
{
System.out.println( "INFO - Running the `run` method of our ExpiringTask. Now: " + Instant.now() );
if ( Instant.now().isAfter( this.timeLimit ) )
{
System.out.println( "☑️ Mission accomplished. Our task has expired. Now: " + Instant.now() );
}
else
{
System.out.println( "⏲️ Not yet expired. Reschedule to check again. Now: " + Instant.now() );
this.scheduledExecutorService.schedule( this , this.untilNextRun.toNanos() , TimeUnit.NANOSECONDS );
}
}
}
In real work, when rescheduling the task, I would check that the executor service is still available. To do so, make a call to ExecutorSerivce#isShutdown. This step is omitted in code above for brevity and simplicity.
How to run get that task running? Here is the main chunk of an app to demonstrate this task running repeatedly while rescheduling itself.
First we establish our target time limit as five seconds from now. Then we specify waiting between runs for one second each time. And we get a scheduled executor service. Be sure to keep that reference around, as you must be careful to gracefully shut down the executor service eventually — otherwise its backing thread pool may continue running indefinitely, like a zombie ♂️.
Next step is to instantiate our task, passing the pieces to its constructor. Finally, to get things working, we tell the scheduled executor service to run our task after a certain amount of delay. For this first run we want to run immediately, so we specify a delay of zero nanoseconds.
Instant timeLimit = Instant.now().plus( Duration.ofSeconds( 5 ) );
Duration untilNextRun = Duration.ofSeconds( 1 );
ScheduledExecutorService ses = Executors.newSingleThreadScheduledExecutor();
ExpiringTask task = new ExpiringTask( timeLimit , untilNextRun , ses );
ses.schedule( task , 0 , TimeUnit.NANOSECONDS );
That covers the main idea. Here is the complete app class containing that code excerpted directly above.
package work.basil.example.concurrency;
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class KeepRunningUntil
{
public static void main ( String[] args )
{
KeepRunningUntil app = new KeepRunningUntil();
app.demo();
System.out.println( "INFO - Demo ending. Now: " + Instant.now() );
}
private void demo ( )
{
System.out.println( "INFO - 'demo' method starting. " + Instant.now() );
Instant timeLimit = Instant.now().plus( Duration.ofSeconds( 5 ) );
Duration untilNextRun = Duration.ofSeconds( 1 );
ScheduledExecutorService ses = Executors.newSingleThreadScheduledExecutor();
ExpiringTask task = new ExpiringTask( timeLimit , untilNextRun , ses );
ses.schedule( task , 0 , TimeUnit.NANOSECONDS );
// Wait a while for our background task to do its thing.
try { Thread.sleep( Duration.ofSeconds( 10 ).toMillis() ); } catch ( InterruptedException e ) { throw new RuntimeException( e ); }
this.shutdownAndAwaitTermination( ses );
System.out.println( "INFO - 'demo' method ending. " + Instant.now() );
}
private void shutdownAndAwaitTermination ( ExecutorService executorService )
{
// Boilerplate taken from Javadoc.
// https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/ExecutorService.html#isShutdown()
executorService.shutdown(); // Disable new tasks from being submitted
try
{
// Wait a while for existing tasks to terminate
if ( ! executorService.awaitTermination( 60 , TimeUnit.SECONDS ) )
{
executorService.shutdownNow(); // Cancel currently executing tasks
// Wait a while for tasks to respond to being cancelled
if ( ! executorService.awaitTermination( 60 , TimeUnit.SECONDS ) )
{ System.err.println( "Pool did not terminate" ); }
}
}
catch ( InterruptedException ex )
{
// (Re-)Cancel if current thread also interrupted
executorService.shutdownNow();
// Preserve interrupt status
Thread.currentThread().interrupt();
}
}
}
When run.
INFO - 'demo' method starting. 2022-08-25T08:16:28.384638Z
INFO - Running the `run` method of our ExpiringTask. Now: 2022-08-25T08:16:28.402471Z
⏲️ Not yet expired. Reschedule to check again. Now: 2022-08-25T08:16:28.402657Z
INFO - Running the `run` method of our ExpiringTask. Now: 2022-08-25T08:16:29.408010Z
⏲️ Not yet expired. Reschedule to check again. Now: 2022-08-25T08:16:29.408126Z
INFO - Running the `run` method of our ExpiringTask. Now: 2022-08-25T08:16:30.413336Z
⏲️ Not yet expired. Reschedule to check again. Now: 2022-08-25T08:16:30.413599Z
INFO - Running the `run` method of our ExpiringTask. Now: 2022-08-25T08:16:31.416144Z
⏲️ Not yet expired. Reschedule to check again. Now: 2022-08-25T08:16:31.416230Z
INFO - Running the `run` method of our ExpiringTask. Now: 2022-08-25T08:16:32.421484Z
⏲️ Not yet expired. Reschedule to check again. Now: 2022-08-25T08:16:32.421873Z
INFO - Running the `run` method of our ExpiringTask. Now: 2022-08-25T08:16:33.425519Z
☑️ Mission accomplished. Our task has expired. Now: 2022-08-25T08:16:33.425863Z
INFO - 'demo' method ending. 2022-08-25T08:16:38.407566Z
INFO - Demo ending. Now: 2022-08-25T08:16:38.407755Z