Question

The following example shows an issue in the ScheduledExecutorService. I am scheduling two tasks "1" and "2" running longer than the schedule interval. Task "2" submits another task to execute only once.

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class TestExecutorFairness {
  public static void main(final String[] args) {

    final int interval = 200;
    final int sleeptime = 600;

    final ScheduledExecutorService executor = Executors
        .newSingleThreadScheduledExecutor();

    // schedule task 1
    executor.scheduleAtFixedRate(new Runnable() {
      @Override
      public void run() {
        try {
          Thread.sleep(sleeptime);
        } catch (final InterruptedException e) {
          e.printStackTrace();
        }

        System.out.println("1");
      }
    }, interval, interval, TimeUnit.MILLISECONDS);

    // schedule task 2
    executor.scheduleAtFixedRate(new Runnable() {
      @Override
      public void run() {
        try {
          Thread.sleep(sleeptime);
        } catch (final InterruptedException e) {
          e.printStackTrace();
        }

        System.out.println("2");

        // submit task 3
        executor.submit(new Runnable() {

          @Override
          public void run() {
            System.out.println("3");
          }
        });
      }
    }, interval, interval, TimeUnit.MILLISECONDS);

  }
}

The output I would expect is something like

1
2
1
2
3

But it is not executed that way. The task "3" is delayed very long but I need it to be executed ASAP.

Is there any way to change this behaviour to more fairness? Or has someone a better solution?

Was it helpful?

Solution

Interesting. It seems counterintuitive, because the JvaDoc of ScheduledExecutorService explicitly mentions

Commands submitted using the Executor.execute(java.lang.Runnable) and ExecutorService submit methods are scheduled with a requested delay of zero

So one could assume that it should be feasible to submit commands like this. But in this case, there are some peculiarities. I can't point my finger at THE exact reason for this behavior, but it's obviously related to

  • The tasks taking longer than the schedule interval
  • The new task being submitted from an executed task
  • The fact that the ScheduledExecutorService internally uses a DelayedWorkQueue
  • Most importantly: That you are using a single-threaded ScheduledExecutorService

A considerable problem might also be that this is filling up the work queue and will sooner or later lead to an OutOfMemoryError. This can also be seen in this (slightly adjusted) example:

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class TestExecutorFairness {
  public static void main(final String[] args) {

    final int interval = 200;
    final int sleeptime = 600;

    final ScheduledExecutorService executor = 
        Executors.newScheduledThreadPool(1);

    final long start = System.currentTimeMillis();

    // schedule task 1
    executor.scheduleAtFixedRate(new Runnable() {
      @Override
      public void run() {
        try {
          Thread.sleep(sleeptime);
        } catch (final InterruptedException e) {
          e.printStackTrace();
        }

        System.out.println("1 at "+(System.currentTimeMillis()-start));
      }
    }, interval, interval, TimeUnit.MILLISECONDS);

    // schedule task 2
    executor.scheduleAtFixedRate(new Runnable() {
      @Override
      public void run() {
        try {
          Thread.sleep(sleeptime);
        } catch (final InterruptedException e) {
          e.printStackTrace();
        }

        System.out.println("2 at "+(System.currentTimeMillis()-start));


        System.out.println("Submitting 3 to "+executor);
        // submit task 3
        executor.submit(new Runnable() {

          @Override
          public void run() {
              System.out.println("3 at "+(System.currentTimeMillis()-start));
          }
        });
      }
    }, interval, interval, TimeUnit.MILLISECONDS);

  }
}

The number of "queued tasks" in the Executor constantly increases.

A solution in this case is rather simple: Instead of a

Executors.newScheduledThreadPool(1)

you can just create a

Executors.newScheduledThreadPool(3)

Of course, this changes the "timing behavior" in this example. I have to assume that the Thread.sleep() in this example was solely intended to simulate a complex computation that did not fit into this example code. But maybe just making sure that the number of threads is at least numberOfPeriodicTasks+1 can also be applied in your real application.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top