JDK-8360593 : CompletableFuture problem in Java 25 when running on 2 or fewer cores
  • Type: Bug
  • Component: core-libs
  • Sub-Component: java.util.concurrent
  • Affected Version: 25
  • Priority: P3
  • Status: Open
  • Resolution: Unresolved
  • OS: generic
  • CPU: generic
  • Submitted: 2025-06-26
  • Updated: 2025-06-27
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
JDK 25
25Unresolved
Related Reports
Causes :  
Description
ADDITIONAL SYSTEM INFORMATION :
Linux running on x64 or AMD and Windows Subsystem for Linux

Currently using Java 25+28 (but we have seen this on earlier builds of Java 25)

A DESCRIPTION OF THE PROBLEM :
We ran across this problem in Java 25 during our standard testing and have simplified it to work outside of our test infrastructure.  It runs fine in previous versions of Java with 2 or fewer cores and runs fine in Java 25 on systems that have more than 2 cores allocated.  We can reproduce locally on our Linux systems if we use taskset to limit the cores to 1 or 2.

REGRESSION : Last worked in version 24.0.1

STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Run the test case code on a system with 1 or 2 cores (or limit the number of cores available to 1 or 2).
For example, in Linux

taskset -c 2-3 java Java25ThenApplyAsyncDoesNotRunOnSeparateThread.java

EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
I am here 1!
I am here 2!
I am here 3!
I am here 4!
I am here 5!
I am here 6!
ACTUAL -
I am here 1!
Exception in thread "main" java.util.concurrent.TimeoutException
        at java.base/java.util.concurrent.CompletableFuture.timedGet(CompletableFuture.java:1981)
        at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2116)
        at Java25ThenApplyAsyncDoesNotRunOnSeparateThread.main(Java25ThenApplyAsyncDoesNotRunOnSeparateThread.java:33)

---------- BEGIN SOURCE ----------
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;


public class Java25ThenApplyAsyncDoesNotRunOnSeparateThread {

    public static void main(String[] args) throws Exception {
        CountDownLatch blocker = new CountDownLatch(1);
        CompletableFuture<String> receiveSuppliedValue = new CompletableFuture<>();

        CompletableFuture<String> thenApplyAsyncCompleted = receiveSuppliedValue
                        .thenApplyAsync(value -> {
                            return value + "Applied";
                        });

        CompletableFuture<String> supplyAsyncCompleted = CompletableFuture
                        .supplyAsync(() -> {
                            receiveSuppliedValue.complete("Supplied");
                            try {
                                if (blocker.await(2, TimeUnit.MINUTES))
                                    return "supplyAsyncCompleted";
                                else
                                    throw new RuntimeException("supplier not allowed to complete");
                            } catch (InterruptedException x) {
                                throw new CompletionException(x);
                            }
                        });
         
        System.out.println("I am here 1!"); 
         
        String result = thenApplyAsyncCompleted.get(15, TimeUnit.SECONDS);
        System.out.println("I am here 2!"); 
        if (!"SuppliedApplied".equals(result)) {
            System.err.println("Wrong value returned " + result);
            System.exit(1);
        }    
        System.out.println("I am here 3!"); 
        // Supplier should still be running:
        if (supplyAsyncCompleted.isDone()) {
            System.err.println("isDone returned true when not expected to be true");
            System.exit(1);
        }    
        System.out.println("I am here 4!"); 
        // Allow the first bean method to complete
        blocker.countDown();
        System.out.println("I am here 5!"); 
        result = supplyAsyncCompleted.get(15, TimeUnit.SECONDS);
        if (!"supplyAsyncCompleted".equals(result)) {
            System.err.println("CompletableFuture did not return expected value " + result);
            System.exit(1);
        }    
        System.out.println("I am here 6!"); 
    }
}
---------- END SOURCE ----------


Comments
Some history: CompletableFuture initially included common.parallelism < 2 check as a better alternative to guaranteed stalls when run in security-managed, parallel-disabled contexts (more than one available processor would otherwise be expected.) The unintended consequence of not bounding the thread pool in these cases was a problem that only emerged when use of taskset etc became common, and would have been addressed by bounding the CF default executor in some other way even without JDK-8319447, to avoid other kinds of failures and failure reports. So it seems that adding a release note is the best course of action.
26-06-2025

The reproducer does demonstrate a behavior change for the common.parallelism = 1 case where the single worker that is blocked/parked (without a managedBlock). The specific case reported can be worked around by running with -Djava.util.concurrent.ForkJoinPool.common.parallelism=N, where N >= 2. Note that the reproducer will deadlock with JDK 24 (and older releases) when common.parallelism=N (N >= 2) and there are N tasks blocked. JDK-8319447 will have a release note.
26-06-2025

The observations on WSL 2: JDK 25ea+16: Passed. JDK 25ea+17: Failed, TimeoutException thrown Impact -> H (Regression) Likelihood -> L (Early access) Workaround -> M (Somewhere in-between the extremes) Priority -> P3
26-06-2025