Consider a simple program:


public static void main(String[] args) throws Exception {
	// Create an ExecutorService with a fixed number of threads: three
	ExecutorService service = Executors.newFixedThreadPool(3);
 
	// Pass a simple Runnable task to the ExecutorService
	service.submit(() -> System.out.println("done"));
}

Running the program produces the console output we expect:

done

But this isn't followed by the output that we usually see in IntelliJ IDEA:

Process finished with exit code 0

We usually see that when a program ends.

Why does that happen?

The description of the newFixedThreadPool() method tells us that threads created using an ExecutorService continue to exist until they are explicitly stopped. That means that because we passed a task to the ExecutorService, a thread was created to execute it, and that thread continues to exist even after the task is done.

Stopping at ExecutorService

As a result, we need to "shut down" (or stop) the ExecutorService. We can do this in two ways:

  1. void shutdown() — after this method is called, the ExecutorService stops accepting new jobs. All tasks previously submitted to the ExecutorService will continue to run.

    
    public static void main(String[] args) throws Exception {
    ExecutorService service = Executors.newFixedThreadPool(3);
        	service.submit(() -> System.out.println("task 1"));
        	service.submit(() -> System.out.println("task 2"));
        	service.shutdown();
        	// A RejectedExecutionException will occur here
        	service.submit(() -> System.out.println("task 3"));
    }
    
  2. List<Runnable> shutdownNow() — This method attempts to stop jobs that are currently active. Tasks that are still waiting for their turn are discarded and returned as a list of Runnables.

    
    public static void main(String[] args) throws Exception {
        ExecutorService service = Executors.newFixedThreadPool(5);
        List.of(1, 2, 3, 4, 5, 6, 7, 8).forEach(i -> service.submit(() -> System.out.println(i)));
        List<Runnable> runnables = service.shutdownNow();
        runnables.forEach(System.out::println);
    }
    

Output:

1
2
4
3
java.util.concurrent.FutureTask@1e80bfe8[Not completed, task = java.util.concurrent.Executors$RunnableAdapter@4edde6e5[Wrapped task = Test$$Lambda$16/0x0000000800b95040@70177ecd]]
java.util.concurrent.FutureTask@cc34f4d[Not completed, task = java.util.concurrent.Executors$RunnableAdapter@66a29884[Wrapped task = Test$$Lambda$16/0x0000000800b95040@4769b07b]]
java.util.concurrent.FutureTask@6f539caf[Not completed, task = java.util.concurrent.Executors$RunnableAdapter@17a7cec2[Wrapped task = Test$$Lambda$16/0x0000000800b95040@65b3120a]]
5

Process finished with exit code 0

The output will differ from run to run. There are 2 kinds of lines in the output:

  • A number means that the ExecutorService managed to process the corresponding task, displaying the number from the list we used to create tasks.

  • The results of calling the toString() method on a FutureTask object. These objects are the tasks that were submitted to the ExecutorService but were not processed.

The output has another interesting nuance. In an ideal world, we would first see all the displayed numbers, followed by the FutureTask objects. But synchronization issues jumble the lines in the output.

Other methods

ExecutorService has several more methods related to stopping it:

  1. boolean awaitTermination(long timeout, TimeUnit unit) — this method blocks the thread that calls it. The block ends as soon as any one of the following three events occurs:

    • after the shutdown() method is called, all active jobs and all scheduled tasks have been executed;
    • the timeout determined by the method parameters has elapsed;
    • the thread that called the awaitTermination() method is terminated.

    The method returns true if the ExecutorService is stopped before the timeout elapses, and false if the timeout had already elapsed.

    
    public static void main(String[] args) throws Exception {
    	ExecutorService service = Executors.newFixedThreadPool(2);
    	service.submit(() -> System.out.println("task 1"));
    	service.submit(() -> System.out.println("task 2"));
    	service.submit(() -> System.out.println("task 3"));
    	service.shutdown();
    	System.out.println(service.awaitTermination(1, TimeUnit.MICROSECONDS));
    }
    
  2. boolean isShutdown() — Returns true if the shutdown() or shutdownNow() method has been called on the ExecutorService.

    
    public static void main(String[] args) throws Exception {
    	ExecutorService service = Executors.newFixedThreadPool(2);
    	service.submit(() -> System.out.println("task 1"));
    	service.submit(() -> System.out.println("task 2"));
    	service.submit(() -> System.out.println("task 3"));
    	System.out.println(service.isShutdown());
    	service.shutdown();
    	System.out.println(service.isShutdown());
    }
    
  3. boolean isTerminated() — Returns true if the shutdown() or shutdownNow() method has been called on the ExecutorService and all tasks are done.

    
    public static void main(String[] args) throws Exception {
        ExecutorService service = Executors.newFixedThreadPool(5);
        List.of(1, 2, 3, 4, 5, 6, 7, 8).forEach(i -> service.submit(() -> System.out.println(i)));
        service.shutdownNow();
        System.out.println(service.isTerminated());
    }
    

Example code that uses these methods:


public static void main(String[] args) throws Exception {
   ExecutorService service = Executors.newFixedThreadPool(16);
   Callable<String> task = () -> {
       Thread.sleep(1);
       return "Done";
   };
 
   // Add 10,000 tasks to the queue
   List<Future<String>> futures = IntStream.range(0, 10_000)
           .mapToObj(i -> service.submit(task))
           .collect(Collectors.toList());
   System.out.printf("%d tasks were submitted for execution.%n", futures.size());
 
   // Attempt to shut down
   service.shutdown();
   // Wait 100 milliseconds to finish the work
   if (service.awaitTermination(100, TimeUnit.MILLISECONDS)) {
       System.out.println("All tasks completed!");
   } else {
       // Stop forcibly
       List<Runnable> notExecuted = service.shutdownNow();
       System.out.printf("%d tasks were not started.%n", notExecuted.size());
   }
 
   System.out.printf("Total tasks completed: %d.%n", futures.stream().filter(Future::isDone).count());
}

Output (differs from run to run):

10,000 tasks were submitted for execution.
9170 tasks were not started.
Total tasks completed: 830 tasks.

Process finished with exit code 0