Поведение ForkJoinPool в CompletableFuture.supplyAsync()

Я сравниваю поведение CompletableFuture.supplyAsync() в двух случаях, когда я устанавливаю пользовательский ExecutorService или я хочу, чтобы мой поставщик выполнялся по умолчанию (если не указан), то есть ForkJoinPool.commonPool()

Давайте посмотрим на разницу:

public class MainApplication {
  public static void main(final String[] args) throws ExecutionException, InterruptedException {

    Supplier<String> action1 = () -> {
        try {
            Thread.sleep(3000);
        }finally {
            return "Done";
        }
    };
    Function<String, String> action2 = (input) -> {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            return input + "!!";
        }
    };

    final ExecutorService executorService = Executors.newFixedThreadPool(4);

    CompletableFuture.supplyAsync(action1, executorService)
                     .thenApply  (action2)
                     .thenAccept (res -> System.out.println(res));

    System.out.println("This is the end of the execution");

  }
}

В этом случае я передаю executorService в мою supplyAsync (), и он печатает:

Это конец казни

Готово!!

Таким образом, "Готово" печатается после окончания основного исполнения.

НО, если я использую вместо этого:

CompletableFuture.supplyAsync(action1)

поэтому я не передаю свой пользовательский executorService, а класс CompletableFuture использует под капотом ForkJoinPool.commonPool(), тогда "Done" вообще не печатается:

Это конец казни

Процесс завершен с кодом выхода 0

Зачем?

2 ответа

Решение

В обоих случаях, когда вы делаете

CompletableFuture.supplyAsync(action1, executorService)
                     .thenApply  (action2)
                     .thenAccept (res -> System.out.println(res));

Вы не ждете завершения задачи. Но тогда ваша программа собирается выйти, и есть различия в том, как общий пул форк-соединений:

ForkJoinPool.commonPool()

и штатное обслуживание исполнителя:

final ExecutorService executorService = Executors.newFixedThreadPool(4);

..реагировать на попытку вызова эквивалента System.exit(...).

Вот что говорит док о общем пуле fork join, на что следует обратить внимание:

Однако этот пул и любая текущая обработка автоматически завершаются по программе System.exit(int). Любая программа, которая полагается на асинхронную обработку задачи для завершения до завершения программы, должна вызывать commonPool(). AwaitQuiescence перед завершением.

Это ссылка на документы ExecutorService, вы можете обратить внимание на:

Метод shutdown() позволит выполнить ранее отправленные задачи перед завершением

Я думаю, что это может быть разница, о которой вы спрашиваете.

ForkJoinPool использует потоки демона, которые не препятствуют выходу JVM. С другой стороны, потоки в ExecutorService, созданные Executors, являются потоками, не являющимися демонами, поэтому он не позволяет JVM завершаться до тех пор, пока вы явно не закроете пул потоков.

Также обратите внимание, что в вашем примере вам необходимо завершить пул в конце, чтобы завершить работу JVM.

executorService.shutdown();

Итак, одним из решений было бы удерживать основной поток в течение нескольких секунд, пока ваши вычисления не будут завершены следующим образом,

Thread.sleep(4000);
Другие вопросы по тегам