Java's CompletableFuture and Threads

19 May, 2017 · 2 min read · #java #multithreading

Quiz time. How many threads (other than main) will this program create on a dual-core machine?

public class App {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            CompletableFuture.runAsync(() -> {
                System.out.println(currentThread().getName());
            }).join();
        }
    }
}

Since we haven’t provided an Executor, one would expect all tasks to be executed in commonPool() and since we wait for previous task to complete before submitting the next, a single thread should be reused. However, you’ll notice that the program creates a new thread for each task!

Why?

The documentation of CompletableFuture says,

All async methods without an explicit Executor argument are performed using the ForkJoinPool.commonPool() (unless it does not support a parallelism level of at least two, in which case, a new Thread is created to run each task).

But aren’t we running on a dual-core machine which should have a parallelism of 2? It turns out that the parallelism of commonPool() is 1 less than available cores, because the calling thread is counted towards total parallelism! So on a dual-core system it is 1 and CompletableFuture’s runAsync method decides to spawn a new thread for each task.

How Did It Matter?

We happened to use a library at work, that stored a reference to all Threads that invoked its method. It assumed that the number of threads created during the lifetime of the process using it will be small and finite (who doesn’t re-use threads?) and never removed these references. When a piece of code that looked like the example above was introduced, we ended-up with a memory leak.

If you liked what you read, consider subscribing to the RSS feed in your favourite feed reader.