статический ScheduledThreadPoolExecutor в CompletableFuture.Delayer

В java-9 был введен новый метод completeOnTimeout в классе CompletableFuture:

public CompletableFuture<T> completeOnTimeout(T value, long timeout,
                                              TimeUnit unit) {
    if (unit == null)
        throw new NullPointerException();
    if (result == null)
        whenComplete(new Canceller(Delayer.delay(
                                       new DelayedCompleter<T>(this, value),
                                       timeout, unit)));
    return this;
}

Я не понимаю, почему он использует статический ScheduledThreadPoolExecutor в своей реализации:

    static ScheduledFuture<?> delay(Runnable command, long delay,
                                    TimeUnit unit) {
        return delayer.schedule(command, delay, unit);
    }

куда

    static final ScheduledThreadPoolExecutor delayer;
    static {
        (delayer = new ScheduledThreadPoolExecutor(
            1, new DaemonThreadFactory())).
            setRemoveOnCancelPolicy(true);
    }

Для меня это очень странный подход, так как он может стать узким местом для всего приложения: единственным из них является ScheduledThreadPoolExecutor с единственным потоком, содержащимся внутри пула для всех возможных задач CompletableFuture?

Что мне здесь не хватает?

PS Это выглядит так:

1) авторы этого кода не хотели извлекать эту логику и предпочли повторно использовать ScheduledThreadPoolExecutor,

2), и это, по-видимому, привело к такому решению со статической переменной, поскольку очень сложно создать новый исполнитель для каждого CompletableFuture.

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

Ответ 1

Вы правы, это может стать узким местом, но не для самого завершения, которое просто устанавливает переменную в CompletableFuture. Эта одна нить может завершить миллионы фьючерсов за секунду. Критический аспект заключается в том, что завершение может инициировать оценку зависимых этапов в завершающей цепочке.

Так

Executor neverDone = r -> {};
long t0 = System.nanoTime();
CompletableFuture<String> c11 =
    CompletableFuture.supplyAsync(() -> "foo", neverDone)
        .completeOnTimeout("timeout", 2, TimeUnit.SECONDS)
        .thenApply(s -> {
            System.out.println("long dependent action 1 "+Thread.currentThread());
            LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(5));
            return s;
        });
CompletableFuture<String> c12 =
    CompletableFuture.supplyAsync(() -> "bar", neverDone)
        .completeOnTimeout("timeout", 2, TimeUnit.SECONDS)
        .thenApply(s -> {
            System.out.println("long dependent action 2 "+Thread.currentThread());
            LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(5));
            return s;
        });
System.out.println("set up");
CompletableFuture.allOf(
    c11.thenAccept(System.out::println),
    c12.thenAccept(System.out::println)
).join();
System.out.println(Math.round((System.nanoTime()-t0)*1e-9)+" s");

распечатает

set up
long dependent action 1 Thread[CompletableFutureDelayScheduler,5,main]
timeout
long dependent action 2 Thread[CompletableFutureDelayScheduler,5,main]
timeout
12 s

Использование методов …Async цепочки будет устранять проблему

Executor neverDone = r -> {};
long t0 = System.nanoTime();
CompletableFuture<String> c11 =
    CompletableFuture.supplyAsync(() -> "foo", neverDone)
        .completeOnTimeout("timeout", 2, TimeUnit.SECONDS)
        .thenApplyAsync(s -> {
            System.out.println("long dependent action 1 "+Thread.currentThread());
            LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(5));
            return s;
        });
CompletableFuture<String> c12 =
    CompletableFuture.supplyAsync(() -> "bar", neverDone)
        .completeOnTimeout("timeout", 2, TimeUnit.SECONDS)
        .thenApplyAsync(s -> {
            System.out.println("long dependent action 2 "+Thread.currentThread());
            LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(5));
            return s;
        });
System.out.println("set up");
CompletableFuture.allOf(
    c11.thenAccept(System.out::println),
    c12.thenAccept(System.out::println)
).join();
System.out.println(Math.round((System.nanoTime()-t0)*1e-9)+" s");

распечатает

set up
long dependent action 2 Thread[ForkJoinPool.commonPool-worker-2,5,main]
long dependent action 1 Thread[ForkJoinPool.commonPool-worker-9,5,main]
timeout
timeout
7 s

Вывод заключается в том, что, когда у вас есть потенциально длительная оценка, вы всегда должны цеплять через один из методов …Async. Учитывая отсутствие контроля над исполняемым потоком при использовании методов без суффикса "... Async" (также может быть поток, вызывающий метод цепочки или любой другой поток, вызывающий "метод завершения", см. Также этот ответ), это что вы всегда должны делать.

Ответ 2

Конечно, это вопрос, на который должны ответить авторы. Во всяком случае, вот мое мнение по этому вопросу.

Я не понимаю, почему он использует статический ScheduledThreadPoolExecutor в своей реализации:

...

Для меня это очень странный подход, так как он может стать узким местом для всего приложения: единственным из них является ScheduledThreadPoolExecutor с единственным потоком, содержащимся внутри пула для всех возможных задач CompletableFuture?

Ты прав. ScheduledThreadPoolExecutor может запускать произвольный код. В частности, orTimeout() и completeOnTimeout() будут вызывать completeExceptionally() и complete(), которые по умолчанию синхронно называют иждивенцев.

Чтобы избежать такого поведения, вы должны использовать свой собственный CompletionStage или подкласс CompletableFuture который делает non- *Async методы *Async всегда вызывают методы *Async. Это намного проще, поскольку Java 9 переопределяет newIncompleteFuture().

Это выглядит как:

1) авторы этого кода не хотели извлекать эту логику и предпочли повторно использовать ScheduledThreadPoolExecutor,

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

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

Если вам требуются отложенные задачи со статическими интервалами, то CompletableFuture.delayedExecutor(), вероятно, достаточно хорош, учитывая небольшие накладные расходы на обертывающие объекты.

Для переменных интервалов есть дополнительные накладные расходы при создании оболочки Executor каждый раз, но на пути уже есть несколько созданных объектов, таких как новые экземпляры внутренних классов Canceller, Timeout, DelayedCompleter и TaskSubmitter.

Как часто нам нужно откладывать многие задачи в переменные интервалы? Чистый асинхронный код может делать это все время для разных тайм-аутов, но поскольку у нас нет самого запланированного исполнителя, мы либо принимаем эти служебные данные, либо используем еще один статический планировщик.

2), и это, по-видимому, привело к такому решению со статической переменной, поскольку очень сложно создать новый исполнитель для каждого CompletableFuture.

Именно так.