Running on Java 26-ea+20-2057 (Preview)
Home of The JavaSpecialists' Newsletter

332ForkJoinPool.asyncCommonPool()

Author: Dr Heinz M. KabutzDate: 2026-01-30Java Version: 25+Sources on GitHubCategory: Concurrency
 

Abstract: The common ForkJoinPool refuses to tell us its exact parallelism, in order to spare us embarrassing division by zero errors. The effect is that the rest of the concurrency framework has had to create dangerous workarounds to counteract a possibly zero sized pool. Since Java 25, the ForkJoinPool will silently create a couple of threads to make CompletableFuture (and others) safer to use. In addition, ForkJoinPool has become a ScheduledExecutorService, thus we can use the common ForkJoinPool as a global timer.

 

A hearty welcome to our 332nd edition of The Java(tm) Specialists' Newsletter, written at 33000 somewhere between Amsterdam and Athens. I spent this week teaching advanced Java concurrency to a group of amazing engineers. I measure my classes by how much I myself learn, and this class did not disappoint. This newsletter is a result of what we discovered this week, looking at the Java 25 code during demos and exercises. Please let me know if you would like to book an inhouse course for your company.

As is my habit, I went for my morning runs and push-ups, but on Thursday, I was a bit concerned when I was greeted by winter wonderland. I do not get much chance to run in snow on Crete. Fortunately, I had packed my trusty Yaktrax running snow chains, and off I went. The running was fine, but I could not get good traction during the push-ups, as my feet kept on slipping back. I've ordered a newer model of the Yaktrax for my next outside snow adventure, but even then, I might need to add some spikes for my toes to stop slipping. As we say - first-world problems :-)

javaspecialists.teachable.com: Please visit our new self-study course catalog to see how you can upskill your Java knowledge.

ForkJoinPool.asyncCommonPool()

Here is a trick question. How long will this code take to complete?

import java.util.concurrent.*;

public class TrickQuestion {
    void main() {
        var time = System.nanoTime();
        try {
            ForkJoinPool.commonPool().submit(() -> {
                var until = System.nanoTime() + 1_000_000_000;
                while (System.nanoTime() <= until) ;
            }).join();
        } finally {
            time = System.nanoTime() - time;
            System.out.printf("time = %dms%n", (time / 1_000_000));
        }
    }
}

Since you were warned that this is a trick question, you probably did not want to answer "one second", even though that is the most likely result. It would be correct, except in the strange case when the common pool parallelism has been set to zero, with -Djava.util.concurrent.ForkJoinPool.common.parallelism=0. This property was allowed so that JEE servers could disable the creation of threads, even for parallel streams. As you probably know, when we create parallel stream, our threads also help do the work. This way, even if the common fork join pool is empty, the parallel stream code will still run, albeit sequentially.

The following code would thus complete in at most as many seconds as we have active processors, even if the common pool is set to zero:

import java.util.stream.*;

public class NotATrickQuestion {
    void main() {
        var time = System.nanoTime();
        try {
            var runtime = Runtime.getRuntime();
            IntStream.range(0, runtime.availableProcessors())
                    .parallel()
                    .forEach(i -> {
                        System.out.println(Thread.currentThread());
                        var until = System.nanoTime() + 1_000_000_000;
                        while (System.nanoTime() <= until) ;
                    });
        } finally {
            time = System.nanoTime() - time;
            System.out.printf("time = %dms%n", (time / 1_000_000));
        }
    }
}

Now it gets interesting. There is no way to determine what the exact number of threads in the common ForkJoinPool is. ForkJoinPool.getCommonPoolParallelism() would typically return the number of hardware threads minus one (since the calling thread also rolls up its sleeves and helps do the work). However, even if the pool has a parallelism set to zero, it still claims that it is one.

This makes things a bit challenging. Imagine if we want to use CompletableFuture.runAsync(), which by default executes in the common ForkJoinPool. What happens if that pool does not have any worker threads? The only way that we could be safe was if the CompletableFuture checked what the number of claimed workers was, and then if it said it was one, to then assume that it was actually zero. Oh and it gets worse. I mentioned that the number of workers is by default the number of hardware threads minus one. Thus if we have a dual-core machine, then the common pool has only one thread. But if we have a single-core machine, it still gets one thread, not zero. The only time when it has zero would be if we explicitly set the parallelism to 0, and then it would lie through its teeth that it actually had 1.

Since the rest of the JDK cannot differentiate between a common ForkJoinPool with one and with zero workers, what are we to do when we want to execute an asynchronous task with CompletableFuture? Up until now, they instead started a new thread for every single task, using this ThreadPerTaskExecutor:

private static final class ThreadPerTaskExecutor implements Executor {
    public void execute(Runnable r) {
        Objects.requireNonNull(r);
        new Thread(r).start();
    }
}

Try the following class in Java 24 and then Java 25:

import java.util.concurrent.*;
import java.util.stream.*;

public class CompletableFutureDemo {
    public static void main(String... args) {
        System.setProperty("java.util.concurrent.ForkJoinPool." +
                "common.parallelism", "0");
        var time = System.nanoTime();
        try {
            var numberOfThreads = IntStream.range(0, 1_000_000)
                    .mapToObj(i -> CompletableFuture.supplyAsync(
                            () -> Thread.currentThread()))
                    .toList()
                    .stream()
                    .map(CompletableFuture::join)
                    .distinct()
                    .count();
            System.out.printf("numberOfThreads = %,d%n", numberOfThreads);
        } finally {
            time = System.nanoTime() - time;
            System.out.printf("time = %dms%n", (time / 1_000_000));
        }
        // Java 24: 1,000,000
        // Java 25: 2
    }
}

In Java 24, it takes 30 seconds on my machine to launch the million threads:

heinz$ java -showversion CompletableFutureDemo.java
openjdk version "24.0.2" 2025-07-15
numberOfThreads = 1,000,000
time = 29756ms

In Java 25, they instead have a special new method that will silently bump up the parallelism to 2 if it finds it at 0:

public class ForkJoinPool {
    // *snip*
    static ForkJoinPool asyncCommonPool() {
        ForkJoinPool cp; int p;
        if ((p = (cp = common).parallelism) == 0)
            U.compareAndSetInt(cp, PARALLELISM, 0, 2);
        return cp;
    }
    // *snip*
}

Here is the output for Java 25:

heinz$ java -showversion CompletableFutureDemo.java
openjdk version "25.0.2" 2026-01-20
numberOfThreads = 2
time = 98ms

Thus it has become safer to use CompletableFuture since Java 25.

In newsletter 311 - Virtual Threads and Parallel Streams, I proposed using the common pool as a safety valve to avoid jamming up the carrier threads with streams. It would be even better to do that using CompletableFuture:

public static <T> T safetyValve(Supplier<T> streamTask) {
    return Thread.currentThread().isVirtual() ?
            CompletableFuture.supplyAsync(streamTask).join() :
            streamTask.get();
}

Common ScheduledExecutorService

Another small change is that in Java 25, the ForkJoinPool now also implements the ScheduledExecutorService interface:

import java.util.concurrent.*;

public class GlobalTimer {
    void main() throws InterruptedException {
        ForkJoinPool.commonPool().scheduleAtFixedRate(
                () -> System.out.println("Cool cat!"),
                1, 1, TimeUnit.SECONDS);
        Thread.sleep(5500);
    }
}

And what happens if we have zero parallelism for the common pool? Yes, you guessed it, it also silently bumps that up to 2, just like with CompletableFuture (and also with SubmissionPublisher).

To end off, here is a DoubleTrickQuestion. What happens when we call this:

import java.util.concurrent.*;

public class DoubleTrickQuestion {
    void main() {
        ForkJoinPool.commonPool()
                .schedule(() -> {}, 0, TimeUnit.DAYS);
        new TrickQuestion().main();
    }
}

Regardless of the parallelism, this will always complete in one second.

Kind regards

Heinz

P.S. Just for fun, I pasted the abstract to this newsletter into an anonymous ChatGPT prompt and asked whether it was written by AI. It said "Human-written: ~65%". On further prompting, it insisted that it was definitely the style of a particular Heinz Kabutz, author of The Java(tm) Specialists' Newsletter. So there you go - handcrafted tech humour, just for you :-)

 

Comments

We are always happy to receive comments from our readers. Feel free to send me a comment via email or discuss the newsletter in our JavaSpecialists Slack Channel (Get an invite here)

When you load these comments, you'll be connected to Disqus. Privacy Statement.

Related Articles

Browse the Newsletter Archive

About the Author

Heinz Kabutz Java Conference Speaker

Java Champion, author of the Javaspecialists Newsletter, conference speaking regular... About Heinz

Superpack

Java Specialists Superpack Our entire Java Specialists Training in one huge bundle more...

Free Java Book

Dynamic Proxies in Java Book
Java Training

We deliver relevant courses, by top Java developers to produce more resourceful and efficient programmers within their organisations.

Java Consulting

We can help make your Java application run faster and trouble-shoot concurrency and performance bugs...