Why does CompletableFuture use ThreadPerTaskExecutor?

some-java-user-99206970363698485155 at vodafonemail.de some-java-user-99206970363698485155 at vodafonemail.de
Wed Aug 17 21:07:17 UTC 2022


Hello,
when ForkJoinPool.getCommonPoolParallelism() is < 2, CompletableFuture uses a ThreadPerTaskExecutor. As the name implies,
that executor creates a new thread per task.
My question is, why is it implemented this way?

This approach can cause drastic performance decreases for applications making heavy use of CompletableFuture. For example when
I ran a program on a machine with 2 CPU cores, the program took ~40 minutes. After manually specifying an executor for CompletableFuture
which simply runs the tasks in the current thread, the program took ~5 minutes. I assume this was caused by the extensive context
switching for hundreds of threads running concurrently.

A similar extreme case occurred for Minecraft Java Edition in the past where this caused the game to run out of memory because it
created too many threads (https://bugs.mojang.com/browse/MC-137353): OutOfMemoryError: unable to create new native thread

This is tracked already by https://bugs.openjdk.org/browse/JDK-8213115, but unfortunately it looks like that report received little attention
so far. I am a bit afraid that a lot of applications are negatively affected by this (also on GitHub hosted workflow runners which run with
2 CPU cores), but in most cases application developers dismiss this as general performance issues (possibly assuming the hardware
is to blame) without drawing the connection to CompletableFuture.

After digging a bit in the Git history, it appears this behavior was introduced by b1a10b8ed7bedb27ae25341602319a11a1225ee7 to
fix test failures for CompletableFuture/Basic.java (JDK-8020435; private report so I don't know the details). However, wouldn't it be more
reasonable to simply use a thread pool with 2 worker threads? That would match the behavior of a machine with 3 CPU cores and is
known to work without issues (otherwise you would have special casing for this scenario as well in the CompletableFuture implementation).

Sorry if bumping JDK bug reports here is not appreciated. After all I solved this issue by simply not using commonPool(), but I am
afraid others are negatively affected by this as well without being aware of this.

Kind regards



More information about the core-libs-dev mailing list