简体   繁体   中英

Graceful shutdown of ThreadPoolExecutor when Enclosing class is ready for GC

I am using ExecutorService in my class to async a few Callable tasks and then once all the tasks are complete, complete the parent process. Something like this

import java.util.*;
import java.util.concurrent.*;
import java.util.stream.Collectors;

class SampleService implements Callable<String>{

private String dayOfWeek;

public SampleService(String dayOfWeek) {
    this.dayOfWeek = dayOfWeek;
}

@Override
public String call() throws Exception {
    if("Holiday".equals(dayOfWeek)){
        throw new RuntimeException("Holiday is not valid day of week");
    } else{
        Random random = new Random();
        int sleepTime = random.nextInt(60000);
        Thread.sleep(sleepTime);
        System.out.println("Thread "+dayOfWeek+" slept for "+sleepTime);
        return dayOfWeek+" is complete";
    }

}
}

class Scratch {
static ExecutorService executor = null;
public Scratch() {
    executor = Executors.newFixedThreadPool(8);
}

public static void main(String[] args) {
    
    List<String> days = Arrays.asList("Monday","Tuesday","Wednesday","Thursday","Friday","Holiday");
    List<Future<String>> completables = days.stream()
            .map(p -> createFuture(p,executor))
            .collect(Collectors.toList());


    long startTime =  System.currentTimeMillis();

    while(true || (System.currentTimeMillis()-startTime) < 60000){
        boolean complete = true;
        for(Future<String> future : completables){
            complete = complete && future.isDone(); // check if future is done
        }
        if(complete){
            System.out.println(" all tasks complete");
            break;
        }
    }

    long endTime =  System.currentTimeMillis();
    System.out.println("Time taken to get response from all threads "+ (endTime - startTime));

    try{
        for(Future<String> future : completables){
            String text  = future.get();
            System.out.println(text);
        }
    } catch (ExecutionException e) {
        e.printStackTrace();
    } catch (InterruptedException e) {
        e.printStackTrace();
    } 
}

private static Future<String> createFuture(String p, ExecutorService executor) {
    SampleService service = new SampleService(p);
    return executor.submit(service);
}


}

It is working as expected.

The above example is just from a scratch file but i have something very similar. now i have kept the ThreadPoolExecutor as an instance object as it is being called multiple times and i do not want to create a new executor for each call. I would like to know are there any implications if i do not terminate or shutdown the executor when the main service class is terminated/ready for GC. I wanted to use the finalize method but it is deprecated now. So in this case what is the best approach to shutdown the executor when the enclosing class is GC without using finalize method?

Shutting down the ExecutorService

Yes, it is important to shutdown the executor when it is no longer needed. In this example (once the NullPointerException is fixed), when the main method exits, the process does not terminate because the thread pool has not been shut down. As the documentation for Executors.newFixedThreadPool says:

The threads in the pool will exist until it is explicitly shutdown .

You are correct that the use of the finalize method is deprecated and will be removed in a future version of Java. The best approach to shutting it down is to explicitly call shutdown when it is no longer needed, in a finally block to ensure it happens regardless of whether execution completes successfully or throws:

public static void main(String[] args) {
    ExecutorService executor = Executors.newFixedThreadPool(8);

    try {
        List<String> days = Arrays.asList("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Holiday");
        List<Future<String>> completables = days.stream()
                .map(p -> createFuture(p, executor))
                .collect(Collectors.toList());

// code omitted for brevity

        try {
            for (Future<String> future : completables) {
                String text = future.get();
                System.out.println(text);
            }
        } catch (ExecutionException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    } finally {
        executor.shutdown();
    }
}

In Java 19 or later, you can also use the try-with-resources construct:

public static void main(String[] args) {
    try (ExecutorService executor = Executors.newFixedThreadPool(8)) {
        List<String> days = Arrays.asList("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Holiday");
        List<Future<String>> completables = days.stream()
                .map(p -> createFuture(p, executor))
                .collect(Collectors.toList());

// code omitted for brevity

        try {
            for (Future<String> future : completables) {
                String text = future.get();
                System.out.println(text);
            }
        } catch (ExecutionException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

In a more complete application, the application component that creates the ExecutorService may require its own shutdown method to trigger the shutdown of the ExecutorService . It's common for this requirement to cascade through several layers of the application. Most application frameworks support some sort of component lifecycle management for this reason.

Other issues

There are other issues with the code provided:

  1. while(true || (System.currentTimeMillis()-startTime) < 60000) is equivalent to while (true) . The (System.currentTimeMillis()-startTime) < 60000 condition has no effect.
  2. Checking each Future for completion in a loop is called "busy waiting" , and is usually discouraged because it consumes a high level of CPU. If you watch a CPU monitor while running this program, you will notice a spike in usage, despite the fact that the program doesn't do very much other than wait. It is generally preferable to use a method that blocks the calling thread until the condition is satisfied, when possible. In this case, Future.get does exactly that.
  3. Catching and ignoring InterruptedException is not recommended, as it defeats the purpose of interrupting a thread as a signal to terminate. In this case, I would allow it to propagate and terminate the program.

Putting these together, here is the alternative implementation:

public static void main(String[] args) throws InterruptedException {
    ExecutorService executor = Executors.newFixedThreadPool(8);

    try {
        List<String> days = Arrays.asList("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Holiday");
        List<Future<String>> completables = days.stream()
                .map(p -> createFuture(p, executor))
                .collect(Collectors.toList());

        long startTime = System.currentTimeMillis();

        for (Future<String> future : completables) {
            try {
                future.get();
            } catch (ExecutionException e) {
                // ignore it here, it will be printed below
            }
        }
        System.out.println(" all tasks complete");

        long endTime = System.currentTimeMillis();
        System.out.println("Time taken to get response from all threads " + (endTime - startTime));

        try {
            for (Future<String> future : completables) {
                String text = future.get();
                System.out.println(text);
            }
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    } finally {
        executor.shutdown();
    }
}

This preserves the behaviour of printing the results all at once at the end, rather than printing each result in order as it becomes available. If the latter behaviour is acceptable, then you can simplify this further into a single loop.

I would like to know are there any implications if i do not terminate or shutdown the executor when the main service class is terminated/ready for GC.

You certainly should be using a single ExecutorService . That's the whole point of it. You should be submitting your jobs to the service until you are done and then you should immediately shutdown the service.

List<Future<String>> completables = days.stream()
        .map(p -> createFuture(p,executor))
        .collect(Collectors.toList());
// this will shutdown the executor while the submitted jobs run in the background
executor.shutdown();

Also you really done need the done checking look. Going through your futures and calling get() will do that for you:

// you don't need this
for(Future<String> future : completables){
    // also don't use this sort of logic.  works fine.  very hard to read.
    complete = complete && future.isDone(); // check if future is done
}

Also, I don't think you should be using the term GC. You should talk about the main method exiting but we really rarely have to worry about GC issues unless your goal really is to reduce the object bandwidth of a high performance program or if you have a memory leak.

One additional comment. You should never initialize static fields in an instance constructor because if you call new Scratch() twice, it will overwrite the static field.

class Scratch {
    // should be initialized here or in static { } block
    // always use final if you can
    private final static ExecutorService executor = Executors.newFixedThreadPool(8);
    public Scratch() {
        // you should not initialize static fields in an instance constructor
    }

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM