I’m trying to make a ScheduledExecutorService where only one task is active at a time and only once a task has finished, the next task will begin its delay with an arbitrary delay amount.
As a very simple example of what I mean, take a look at this method. The idea is to schedule 10 Runnables to simulate a countdown from 10-1. Each interval takes one second (imagine this was an arbitrary amount of seconds though, I can’t use scheduleAtFixedRate in my use case).
private ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); public void startCountdown() { for (int i = 10; i > 0; i--) { int countdownNumber = i; scheduler.schedule(() -> { System.out.println(countdownNumber); }, 1, TimeUnit.SECONDS); } }
However, this will simply print all 10 numbers at once, instead of waiting for a second between each value. The only way I can circumvent this (to my knowledge) is calculating the ABSOLUTE delay, as opposed to the relative one.
While it’s possible to calculate the absolute time for each item, it would be quite a hassle. Isn’t there some construct in Java that allows me to queue many items at once, but waits in between each item for the delay to finish, rather than processing every delay at once?
Advertisement
Answer
tl;dr
- Do not use your countdown number to directly schedule your tasks. Have one number for scheduling number of seconds to wait (1,2,3,…) and another number for the countdown (9,8,7,…).
- Use
scheduleAtFixedRate
to schedule your tasks for an increasing number of seconds. No need for executor service to be single-threaded.
Details
Task that re-schedules itself
Isn’t there some construct in Java that allows me to queue many items at once, but waits in between each item for the delay to finish, rather than processing every delay at once?
If you have an arbitrary amount of time not known up front when beginning the scheduling, then you should only run one task at a time. Let the task re-schedule itself.
To enable a task to re-schedule itself, pass a reference to the ScheduledExecutorService
to the task object (your Runnable
or Callable
) as an argument in the constructor. After the task completes its main work, it discovers/calculates the amount of time to elapse for the next run. The task then submits itself (this
) to the passed executor service, along with the amount of time to elapse before the next task execution.
I have already posted Answers on Stack Overflow with code for tasks that re-schedule themselves. I would expect others have as well. Search to learn more.
Regarding the “countdown” aspect of your Question, read on.
Countdown
You have the right approach in using a scheduled executor service. The problem is that you are calling the wrong method on that class.
Your call to schedule
means you are scheduling several tasks to all run after a single second. All those tasks are starting from the moment your call is made. So each runs after one second from your call to schedule
. So the ten tasks are all waiting a second from almost the same moment: ten moments a split-second apart, the split-second being the time it takes for your for
loop to continue.
scheduleAtFixedRate
The method you are looking for is scheduleAtFixedRate
. To quote the doc:
Submits a periodic action that becomes enabled first after the given initial delay, and subsequently with the given period; that is, executions will commence after initialDelay, then initialDelay + period, then initialDelay + 2 * period, and so on.
private ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); public void countdown( ScheduledExecutorService scheduler ) { for ( int i = 1 ; i <= 10 ; i++ ) { int countdownNumber = 10 - i ; // For 9 through 0. Add 1 for 10 through 1. scheduler.scheduleAtFixedRate ( () -> { System.out.println( countdownNumber ) ; } , i , // 1 second, then 2 seconds, then 3 seconds, and so on to 10 seconds. TimeUnit.SECONDS ) ; } } … Eventually shut down your scheduled executor service.
Notice how this approach does not require the ScheduledExecutorService
to be single-threaded.
Full example
Here is a complete example app.
package work.basil.example.countdown; import java.time.Duration; import java.time.Instant; import java.util.Objects; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; public class Countdown { public static void main ( String[] args ) { Countdown app = new Countdown(); app.demo(); } private void demo ( ) { System.out.println( "INFO - Demo start. " + Instant.now() ); ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); // Our code does *not* require the executor service to be single-threaded. But for this particular example, we might as well do it that way. this.countdown( scheduler ); this.shutdownAndAwaitTermination( scheduler , Duration.ofMinutes( 1 ) , Duration.ofMinutes( 1 ) ); System.out.println( "INFO - Demo end. " + Instant.now() ); } public void countdown ( final ScheduledExecutorService scheduler ) { Objects.requireNonNull( scheduler ) ; for ( int i = 1 ; i <= 10 ; i++ ) { int countdownNumber = 10 - i; // For 9 through 0. Add 1 for 10 through 1. scheduler.scheduleAtFixedRate ( ( ) -> { System.out.println( "Countdown: " + countdownNumber + " at " + Instant.now() ); } , i , // 1 second, then 2 seconds, then 3 seconds, and so on to 10 seconds. TimeUnit.SECONDS ); } } // My slightly modified version of boilerplate code taken from Javadoc of `ExecutorService`. // https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/ExecutorService.html void shutdownAndAwaitTermination ( final ExecutorService executorService , final Duration waitForWork , final Duration waitForRemainingTasks ) { Objects.requireNonNull( executorService ) ; Objects.requireNonNull( waitForWork ) ; Objects.requireNonNull( waitForRemainingTasks ) ; executorService.shutdown(); // Disable new tasks from being submitted try { // Wait a while for existing tasks to terminate if ( ! executorService.awaitTermination( waitForWork.toMillis() , TimeUnit.MILLISECONDS ) ) { executorService.shutdownNow(); // Cancel currently executing tasks // Wait a while for tasks to respond to being cancelled if ( ! executorService.awaitTermination( waitForRemainingTasks.toMillis() , TimeUnit.MILLISECONDS ) ) { System.err.println( "ExecutorService did not terminate." ); } } } catch ( InterruptedException ex ) { // (Re-)Cancel if current thread also interrupted executorService.shutdownNow(); // Preserve interrupt status Thread.currentThread().interrupt(); } System.out.println( "DEBUG - shutdownAndAwaitTermination ran. " + Instant.now() ); } }
When run:
INFO - Demo start. 2023-01-20T21:24:47.379244Z Countdown: 9 at 2023-01-20T21:24:48.390269Z Countdown: 8 at 2023-01-20T21:24:49.390045Z Countdown: 7 at 2023-01-20T21:24:50.389957Z Countdown: 6 at 2023-01-20T21:24:51.386468Z Countdown: 5 at 2023-01-20T21:24:52.390168Z Countdown: 4 at 2023-01-20T21:24:53.386538Z Countdown: 3 at 2023-01-20T21:24:54.387583Z Countdown: 2 at 2023-01-20T21:24:55.386705Z Countdown: 1 at 2023-01-20T21:24:56.389490Z Countdown: 0 at 2023-01-20T21:24:57.387566Z DEBUG - shutdownAndAwaitTermination ran. 2023-01-20T21:24:57.391224Z INFO - Demo end. 2023-01-20T21:24:57.391966Z
By the way, know that scheduled tasks do not always fire exactly on time for a variety of reasons.
Also, be aware that messages sent to System.out
across threads do not always appear on the console chronologically. If you care about order, always include and study a timestamp such as Instant#now
.