Why do you need the Executor interface?

Prior to Java 5, you had to write all your own code thread management in your application. In addition, creating a new Thread object is a resource-intensive operation, and it doesn't make sense to create a new thread for every lightweight task. And because this problem is familiar to absolutely every developer of multi-threaded applications, they decided to bring this functionality into Java as the Executor framework.

What's the big idea? It's simple: instead of creating a new thread for each new task, threads are kept in a kind of "storage", and when a new task arrives, we retrieve an existing thread instead of creating a new one.

The main interfaces of this framework are Executor, ExecutorService and ScheduledExecutorService, each of which extends the functionality of the previous one.

The Executor interface is the base interface. It declares a single void execute(Runnable command) method that is implemented by a Runnable object.

The ExecutorService interface is more interesting. It has methods for managing the completion of work, as well as methods for returning some kind of result. Let's take a closer look at its methods:

Method Description
void shutdown(); Calling this method stops the ExecutorService. All tasks that have already been submitted for processing will be completed, but new tasks will not be accepted.
List<Runnable> shutdownNow();

Calling this method stops the ExecutorService. Thread.interrupt will be called for all tasks that have already been submitted for processing. This method returns a list of queued tasks.

The method does not wait for the completion of all tasks that are "in progress" at the time the method is called.

Warning: Calling this method may leak resources.

boolean isShutdown(); Checks whether the ExecutorService is stopped.
boolean isTerminated(); Returns true if all tasks were completed following shutdown of the ExecutorService. Until shutdown() or shutdownNow() is called, it will always return false.
boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;

After the shutdown() method is called, this method blocks the thread on which it is called, until one of the following conditions is true:

  • all scheduled tasks are complete;
  • the timeout passed to the method has elapsed;
  • the current thread is interrupted.

Returns true if all tasks are complete, and false if the timeout elapses before termination.

<T> Future<T> submit(Callable<T> task);

Adds a Callable task to the ExecutorService and returns an object that implements the Future interface.

<T> is the type of the result of the passed task.

<T> Future<T> submit(Runnable task, T result);

Adds a Runnable task to the ExecutorService and returns an object that implements the Future interface.

The T result parameter is what gets returned by a call to the get() method on the resulting Future object.

Future<?> submit(Runnable task);

Adds a Runnable task to the ExecutorService and returns an object that implements the Future interface.

If we call the get() method on the resulting Future object, then we get null.

<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException;

Passes a list of Callable tasks to the ExecutorService. Returns a list of Futures from which we can get the result of the work. This list is returned when all submitted tasks are completed.

If the tasks collection is modified while the method is running, the result of this method is undefined.

<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException;

Passes a list of Callable tasks to the ExecutorService. Returns a list of Futures from which we can get the result of the work. This list is returned when all passed tasks are completed, or after the timeout passed to the method has elapsed, whichever comes first.

If the timeout elapses, unfinished tasks are canceled.

Note: It is possible that a canceled task will not stop running (we will see this side effect in the example).

If the tasks collection is modified while the method is running, the result of this method is undefined.

<T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException;

Passes a list of Callable tasks to the ExecutorService. Returns the result of one of the tasks (if any) that completed without throwing an exception (if any).

If the tasks collection is modified while the method is running, the result of this method is undefined.

<T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;

Passes a list of Callable tasks to the ExecutorService. Returns the result of one of the tasks (if any) that completed without throwing an exception before the timeout passed to the method has elapsed.

If the tasks collection is modified while the method is running, the result of this method is undefined.

Let's look at a small example of working with ExecutorService.


import java.util.List;
import java.util.concurrent.*;

public class ExecutorServiceTest {
   public static void main(String[] args) throws InterruptedException, ExecutionException, TimeoutException {
//Create an ExecutorService for 2 threads
       java.util.concurrent.ExecutorService executorService = new ThreadPoolExecutor(2, 2, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10));
// Create 5 tasks
       MyRunnable task1 = new MyRunnable();
       MyRunnable task2 = new MyRunnable();
       MyRunnable task3 = new MyRunnable();
       MyRunnable task4 = new MyRunnable();
       MyRunnable task5 = new MyRunnable();

       final List<MyRunnable> tasks = List.of(task1, task2, task3, task4, task5);
// Pass a list that contains the 5 tasks we created
       final List<Future<Void>> futures = executorService.invokeAll(tasks, 6, TimeUnit.SECONDS);
       System.out.println("Futures received");

// Stop the ExecutorService
       executorService.shutdown();

       try {
           TimeUnit.SECONDS.sleep(3);
       } catch (InterruptedException e) {
           e.printStackTrace();
       }

       System.out.println(executorService.isShutdown());
       System.out.println(executorService.isTerminated());
   }

   public static class MyRunnable implements Callable<Void> {

       @Override
       public void call() {
// Add 2 delays. When the ExecutorService is stopped, we will see which delay is in progress when the attempt is made to stop execution of the task
           try {
               TimeUnit.SECONDS.sleep(3);
           } catch (InterruptedException e) {
               System.out.println("sleep 1: " + e.getMessage());
           }
           try {
               TimeUnit.SECONDS.sleep(2);
           } catch (InterruptedException e) {
               System.out.println("sleep 2: " + e.getMessage());
           }
           System.out.println("done");
           return null;
       }
   }
}

Output:

done
done
Futures received
sleep 1: sleep interrupted
sleep 1: sleep interrupted
done
done
true
true

Each task runs for 5 seconds. We created a pool for two threads, so the first two lines of output make perfect sense.

Six seconds after the program starts, the invokeAll method times out and the result is returned as a list of Futures. This can be seen from the output string Futures received.

After the first two tasks are done, two more begin. But because the timeout set in the invokeAll method elapses, these two tasks don't have time to complete. They receive a "cancel" command. That's why the output has two lines with sleep 1: sleep interrupted.

And then you can see two more lines with done. This is the side effect that I mentioned when describing the invokeAll method.

The fifth and final task never even gets started, so we don't see anything about it in the output.

The last two lines are the result of calling the isShutdown and isTerminated methods.

It is also interesting to run this example in debug mode and look at the task status after the timeout elapses (set a breakpoint on the line with executorService.shutdown();):

We see that two tasks Completed normally, and three tasks were "Cancelled".

ScheduledExecutorService

To conclude our discussion of executors, let's take a look at ScheduledExecutorService.

It has 4 methods:

Method Description
public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit); Schedules the passed Runnable task to run once after the delay specified as an argument.
public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit); Schedules the passed Callable task to run once after the delay specified as an argument.
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit); Schedules periodic execution of the passed task, which will be executed for the first time after initialDelay, and each subsequent run will begin after period.
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit); Schedules periodic execution of the passed task, which will be executed for the first time after initialDelay, and each subsequent run will begin after delay (the period between the completion of the previous run and the start of the current one).