Асинхронное выполнение HTTP-клиента Java 11

Я пытаюсь использовать новый клиентский API HTTP от JDK 11, в частности его асинхронный способ выполнения запросов. Но есть кое-что, что я не уверен, что понимаю (вроде аспекта реализации). В документации говорится:

Асинхронные задачи и зависимые действия возвращенных экземпляров CompletableFuture выполняются в потоках, предоставленных клиентом Executor, где это практически возможно.

Насколько я понимаю, это означает, что если я создаю пользовательский исполнитель при создании объекта HttpClient:

ExecutorService executor = Executors.newFixedThreadPool(3);

HttpClient httpClient = HttpClient.newBuilder()
                      .executor(executor)  // custom executor
                      .build();

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

httpClient.sendAsync(request, BodyHandlers.ofString())
          .thenAccept(response -> {
      System.out.println("Thread is: " + Thread.currentThread().getName());
      // do something when the response is received
});

Тем не менее, в зависимом действии выше (потребитель в thenAccept), я вижу, что поток делает это из общего пула, а не из пользовательского исполнителя, так как он печатает Thread is: ForkJoinPool.commonPool-worker-5.

Это ошибка в реализации? Или что-то мне не хватает? Я замечаю, что он говорит: "экземпляры выполняются в потоках, предоставленных клиентом Executor, где это практично ", так ли это случай, когда это не применяется?

Обратите внимание, что я также попробовал thenAcceptAsync и это тот же результат.

Ответ 1

Я только что нашел обновленную документацию (та, с которой я изначально был связан, кажется старым), где объясняет это поведение реализации:

В общем случае асинхронные задачи выполняются либо в потоке, вызывающем операцию, например, при отправке HTTP-запроса, так и в потоках, предоставленных исполнителем клиента. Зависимые задачи, вызванные возвращаемыми CompletionStages или CompletyFutures, которые явно не указывают исполнителя, выполняются в том же исполнителе по умолчанию, что и для CompletableFuture, или вызывающий поток, если операция завершается до регистрации зависимой задачи.

И стандартный исполнитель CompletableFuture является общим пулом.

Я также нашел идентификатор ошибки, который вводит это поведение, в котором разработчики API полностью объясняют это:

2) Зависимые задачи, выполняемые в общем пуле Выполнение зависимых задач по умолчанию было обновлено для выполнения в том же самом исполнителе, что и для CompletableFuture defaultExecutor. Это более знакомо разработчикам, которые уже используют CF, и уменьшает вероятность того, что HTTP-клиент будет голоден от потоков для выполнения своих задач. Это просто поведение по умолчанию, и HTTP-клиент, и CompletableFuture позволяют при необходимости более мелкозернистое управление.

Ответ 2

Краткая версия. Я думаю, что вы определили детали реализации и что "где это практично" означает, что нет гарантии, что предоставленный executor будет использоваться.

В деталях:

Я загрузил источник JDK 11 здесь. (jdk11-f729ca27cf9a на момент написания этой статьи).

В src/java.net.http/share/classes/jdk/internal/net/http/HttpClientImpl.java существует следующий класс:

/**
 * A DelegatingExecutor is an executor that delegates tasks to
 * a wrapped executor when it detects that the current thread
 * is the SelectorManager thread. If the current thread is not
 * the selector manager thread the given task is executed inline.
 */
final static class DelegatingExecutor implements Executor {

Этот класс использует executor если isInSelectorThread является истинным, иначе задача выполняется в строке. Это сводится к:

boolean isSelectorThread() {
    return Thread.currentThread() == selmgr;
}

где selmgr является SelectorManager. Изменить: этот класс также содержится в HttpClientImpl.java:

// Main loop for this client selector
private final static class SelectorManager extends Thread {

Результат: я предполагаю, что практическое означает, что он зависит от реализации и что нет гарантии, что предоставленный executor будет использоваться.

ПРИМЕЧАНИЕ. Это отличается от исполнителя по умолчанию, когда строитель не предоставляет executor. В этом случае код явно создает новый пул кэшированных потоков. Иными словами, если строитель предоставляет executor, выполняется проверка личности для SelectorManager.