4

Currently I'm using an Executors.newSingleThreadScheduledExecutor() to schedule a task periodically. The ScheduledExecutorService provides two options for this, namely:

  1. ScheduledExecutorService#scheduleWithFixedDelay(…)
  2. ScheduledExecutorService#scheduleAtFixedRate(…)

A Timer has very similar methods that do the same thing. The problem is that none of those do exactly what I want.

The task that I want to schedule will often take a significant part of the period and sometimes even exceed it (depending on how much work there is currently).

  • I don't want to use #scheduleWithFixedDelay because in this case the task computation time and the period will add up, thus the period will almost always be too long (and depend on the task computation time), even if the period isn't exceeded.
  • So it seems like #scheduleAtFixedRate is what I want. However, consider the case where there are several subsequent periods with too long task computation times, or consider a single period with a task computation time that has an order of magnitude of multiple periods. This will result in multiple periods that are too short afterwards because the ScheduledExecutorService or the Timer tries to catch up. The delay can grow indefinitely and affect many periods afterwards, causing a busy CPU when it's maybe not even necessary anymore.

What I want is #scheduleAtFixedRate but the period should never be shorter than specified (it shouldn't try to catch up). With the following code I want to demonstrate this with an example.

public final class Test {
    private static int i = 0;

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        System.out.println((System.currentTimeMillis() - start) + ": " + i);
        ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
        scheduledExecutorService.scheduleWithFixedDelay(() -> {
            i++;

            if (i == 1) {
                try {
                    Thread.sleep(5000L);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    e.printStackTrace();
                }
            }

            System.out.println((System.currentTimeMillis() - start) + ": " + i);
        }, 0L, 1000L, TimeUnit.MILLISECONDS);
    }
}

The output of this code using #scheduleWithFixedDelay is something like this.

0: 0
5012: 1
6018: 2
7024: 3
8025: 4
9029: 5
10037: 6
11042: 7
12050: 8
...

The output of this code using #scheduleAtFixedRate is something like this.

0: 0
5024: 1
5024: 2
5024: 3
5024: 4
5025: 5
5025: 6
6025: 7
7024: 8
...

(Ignore the least significant time digits.) What I actually want is a #schedule method that bahaves like this.

0: 0
5000: 1
5000: 2
6000: 3
7000: 4
8000: 5
9000: 6
10000: 7
11000: 8
...

I think this is similar to how game loops work. The question is, does Java have an inbuilt way, similar to ScheduledExecutorService or Timer, to schedule a task like that. If not, is there an easy way to implement this or are there any external libraries that I can use for this, without reinventing the wheel?

Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
stonar96
  • 1,359
  • 2
  • 11
  • 39
  • 1
    Be careful not to post paragraphs with line-breaks. I fixed some of them. And I broke up your lengthy paragraph into logical chunks — hopefully, please verify that my edits make sense. – Basil Bourque Apr 02 '22 at 01:05
  • You seem to ask for the impossible. Using a **single thread** executor, if the task starting at time 5000 takes 3000, how shall the executor start a task at 6000. That single thread just does not get a chance to schedule another task before 8000. You seem be asking for `newScheduledThreadPool`, though be aware that if on long term average your tasks take longer than the rate you want, you'll always eventually run out of threads. – Harald Apr 03 '22 at 12:17
  • @Harald sorry, but you seem to have completely misunderstood the question. What I'm looking for is easy to implement. E.g. `while (true) { long start = time(); task.run(); long sleep = period - time() + start; if (sleep > 0) sleep(sleep); }`. I'm just looking for a `Timer`- or `ScheduledExecutorService`-like implementation of this. – stonar96 Apr 03 '22 at 12:39

1 Answers1

1

It turns out that I have a wrong assumption in my question.

A Timer has very similar methods that do the same thing.

In fact, this isn't true. Although the Java API Docs state that Timer#schedule(TimerTask, long, long)

Schedules the specified task for repeated fixed-delay execution,

it doesn't do the same thing as ScheduledThreadPoolExecutor#scheduleWithFixedDelay(…). See the following comparison when executing the code from the question with all methods.

|-------|----------|----------|
|       | STPE     | Timer    |
|-------|----------|----------|
|       | 0: 0     | 0: 0     |
|       | 5012: 1  | 5007: 1  |
|       | 6018: 2  | 5007: 2  | <--
|       | 7024: 3  | 6019: 3  |
| Fixed | 8025: 4  | 7019: 4  |
| delay | 9029: 5  | 8022: 5  |
|       | 10037: 6 | 9024: 6  |
|       | 11042: 7 | 10027: 7 |
|       | 12050: 8 | 11042: 8 |
|       | ...      | ...      |
|-------|----------|----------|
|       | 0: 0     | 0: 0     |
|       | 5024: 1  | 5014: 1  |
|       | 5024: 2  | 5014: 2  |
|       | 5024: 3  | 5014: 3  |
| Fixed | 5024: 4  | 5015: 4  |
| rate  | 5025: 5  | 5015: 5  |
|       | 5025: 6  | 5015: 6  |
|       | 6025: 7  | 6002: 7  |
|       | 7024: 8  | 7002: 8  |
|       | ...      | ...      |
|-------|----------|----------|

Thus Timer#schedule(TimerTask, long, long) is in fact what I'm looking for. The difference is that ScheduledThreadPoolExecutor#scheduleWithFixedDelay(…) adds a fixed delay between the completion of the current task and the execution of the next task (this is bad because the task computation time always affects the effective period), whereas Timer#schedule(TimerTask, long, long) re-schedules the next task at the current time (before executing the current task) + a period later. (In contrast, the fixed rate methods don't use the current time + a period, but always the planned execution time + a period. This leads to a burst of ticks when running behind a long time.)

However, although Timer#schedule(TimerTask, long, long) satisfies my requirements, it isn't an ideal solution in my opinion either. The best solution is to use fixed rate scheduling as long as the task computation time doesn't exceed the specified period, and only if it does, the subsequent period is executed based on fixed delay scheduling, discarding the delay.

However, this isn't possible with the JDK's ScheduledThreadPoolExecutor and Timer because they both habe only two hardcoded so-called (in other frameworks) Triggers each, one for fixed rate scheduling and one for fixed delay scheduling. Here are the Triggers of ScheduledThreadPoolExecutor and here are the Triggers of Timer.

The Trigger of ScheduledThreadPoolExecutor for fixed rate scheduling is for example time += p. When this is replaced with another Trigger, namely time = Math.max(System.nanoTime(), time + p), it does exactly what I am asking for in my question, namely scheduling at fixed rate without catching up.

The problem is that it's not possible to schedule tasks with custom Triggers with the JDK's inbuilt options ScheduledThreadPoolExecutor and Timer. Luckily there are some open-source libraries/frameworks, which provide schedulers that support scheduling tasks with custom Triggers.

Examples for such libraries/frameworks are Wisp, Quartz and the Spring Framework. For demonstration I will use the Spring Framework's class ThreadPoolTaskScheduler, which has the method #schedule(Runnable, Trigger) that uses a custom Trigger. For standard fixed rate and fixed delay scheduling there is already an implementation, namely PeriodicTrigger. However, to schedule a task at fixed rate without catching up, one can use the following Trigger.

ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
threadPoolTaskScheduler.setPoolSize(1);
threadPoolTaskScheduler.initialize();
threadPoolTaskScheduler.schedule(task, t -> {
    Date lastExecution = t.lastScheduledExecutionTime();
    Date lastCompletion = t.lastCompletionTime();

    if (lastExecution == null || lastCompletion == null) {
        return new Date(t.getClock().millis() + delay);
    }

    return new Date(Math.max(lastCompletion.getTime(), lastExecution.getTime() + period));
});

When using this scheduler, the result is the same as in the table above for the Timer, when fixed delay scheduling is used, except that this scheduler is much more precise because true fixed rate scheduling is used as much as possible.

stonar96
  • 1,359
  • 2
  • 11
  • 39
  • Warning: The ending example uses the `Date` class to calculate execution delays. This can result in unfortunate behaviors when a user sets the clock, or when protocols like NTP automatically jump or "skew" the clock. If this is all the Spring Framework gives you to work with, shame on them. "Wall clock" scheduling is perfect for "calendaring", but should never be used when you want a fixed rate or delay. For that you should use an "elapsed" time measure like `System.nanoTime` (as used by the ThreadPoolScheduler). – SensorSmith Jul 17 '23 at 16:12