JDK-8274349 : ForkJoinPool.commonPool() does not work with 1 CPU
  • Type: Bug
  • Component: core-libs
  • Sub-Component: java.util.concurrent
  • Affected Version: 17,18
  • Priority: P3
  • Status: Closed
  • Resolution: Fixed
  • OS: generic
  • CPU: generic
  • Submitted: 2021-09-25
  • Updated: 2022-01-12
  • Resolved: 2021-10-04
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 17 JDK 18
17.0.2Fixed 18 b18Fixed
Related Reports
Relates :  
Relates :  
Description
ADDITIONAL SYSTEM INFORMATION :
$ java -version
openjdk version "17" 2021-09-14
OpenJDK Runtime Environment Temurin-17+35 (build 17+35)
OpenJDK 64-Bit Server VM Temurin-17+35 (build 17+35, mixed mode, sharing)

A DESCRIPTION OF THE PROBLEM :
The common pool does not process any submitted tasks if the JVM was started with Runtime.getRuntime().availableProcessors() reporting 1. This can be the case when starting it in a container or by setting the -XX:ActiveProcessorCount=1 flag.

REGRESSION : Last worked in version 16.0.2

STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Run the code given below with `java -XX:ActiveProcessorCount=1 ForkJoinForget`.
"Hello World" won't be printed this way

EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
Tasks submitted to the common pool get executed.
ACTUAL -
Tasks submitted to the common pool aren't executed.

---------- BEGIN SOURCE ----------
import java.util.concurrent.ForkJoinPool;

public class ForkJoinForget {

        public static void main(String[] args) throws Exception {
                ForkJoinPool.commonPool().submit(() -> System.out.println("Hello World"));
                Thread.sleep(1000);
        }
}
---------- END SOURCE ----------

CUSTOMER SUBMITTED WORKAROUND :
One of:
- Not using containers with 1 CPU 
- manually set the property `java.util.concurrent.ForkJoinPool.common.parallelism` to 1
- Not using -XX:ActiveProcessorCount=1 as startup flag

FREQUENCY : always



Comments
From the submitter: I just checked out the latest early access build and it works fine now.
12-10-2021

Requested the submitter verify the fix by downloading the latest version of JDK 18 from https://jdk.java.net/18/
10-10-2021

JDK 17u Fix request: This fixes a regression in JDK 17 so it is important to get it into JDK 17u. The backport applies cleanly and is very low risk.
04-10-2021

Changeset: 2e542e33 Author: David Holmes <dholmes@openjdk.org> Date: 2021-10-04 23:14:12 +0000 URL: https://git.openjdk.java.net/jdk/commit/2e542e33b81a53652956bb5e9636e7f4af5540f7
04-10-2021

[~martin] test update applied to PR.
01-10-2021

While poking at this, I wrote some test improvements. David, could you add these? diff --git a/test/jdk/java/util/concurrent/tck/ForkJoinPoolTest.java b/test/jdk/java/util/concurrent/tck/ForkJoinPoolTest.java index 781b7bffe6d..4703bc51483 100644 --- a/test/jdk/java/util/concurrent/tck/ForkJoinPoolTest.java +++ b/test/jdk/java/util/concurrent/tck/ForkJoinPoolTest.java @@ -51,6 +51,7 @@ import java.util.concurrent.ForkJoinWorkerThread; import java.util.concurrent.Future; import java.util.concurrent.RecursiveTask; import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.ReentrantLock; @@ -223,13 +224,46 @@ public class ForkJoinPoolTest extends JSR166TestCase { /** * getParallelism returns size set in constructor */ - public void testGetParallelism() { - ForkJoinPool p = new ForkJoinPool(1); + public void testGetParallelism_requestedValue() { + int parallelism = ThreadLocalRandom.current().nextInt(1, 4); + ForkJoinPool p = new ForkJoinPool(parallelism); try (PoolCleaner cleaner = cleaner(p)) { - assertEquals(1, p.getParallelism()); + assertEquals(parallelism, p.getParallelism()); } } + private static int availableProcessors() { + return Runtime.getRuntime().availableProcessors(); + } + + /** + * default pool parallelism is availableProcessors() + */ + public void testParallelism_defaultValue() { + ForkJoinPool p = new ForkJoinPool(); + try (PoolCleaner cleaner = cleaner(p)) { + assertEquals(availableProcessors(), p.getParallelism()); + } + } + + /** + * default common pool parallelism is max(1, availableProcessors() - 1) + * But getParallelism() returns 1 when property-requested parallelism is 0. + */ + public void testCommonPoolParallelism_defaultValue() { + if (!testImplementationDetails) return; + + Integer propertyParallelism = + Integer.getInteger( + "java.util.concurrent.ForkJoinPool.common.parallelism"); + + int expectedParallelism = (propertyParallelism == null) + ? Math.max(1, availableProcessors() - 1) + : Math.max(1, propertyParallelism); + assertEquals(expectedParallelism, + ForkJoinPool.commonPool().getParallelism()); + } + /** * getPoolSize returns number of started workers. */
01-10-2021

One way to test running java single-cpu is via taskset 0x1 command .... All current java tests should pass in such an environment - single CPU should be completely supported. Running java tests this way should be a regular QA task. But it is not. Running jsr166 tck tests under taskset 0x1 fails: JUnit Failure: testAwaitQuiescence2(ForkJoinPool8Test): null Fixing all java tests for single-cpu tolerance would be a good project for a test engineer.
01-10-2021

[~dl], [~martin] I'll push this through the PR process. FTR it seems the regression was introduced when this expression: if (parallelism < 0 && // default 1 less than #cores (parallelism = Runtime.getRuntime().availableProcessors() - 1) <= 0) parallelism = 1; was refactored.
30-09-2021

Oh, I hadn't appreciated that jdk17 version was actually incompatible with previous version (not just due to -XX:ActiveProcessorCount). This should be fixed. The obvious patch to jsr166 version is below. Would someone like to commit to openjdk --- ForkJoinPool.java.~1.402.~ 2021-09-30 13:07:08.037109830 -0400 +++ ForkJoinPool.java 2021-09-30 13:23:27.316766510 -0400 @@ -2524,7 +2524,7 @@ * overridden by system properties */ private ForkJoinPool(byte forCommonPoolOnly) { - int parallelism = Runtime.getRuntime().availableProcessors() - 1; + int parallelism = Math.max(1, Runtime.getRuntime().availableProcessors() - 1); ForkJoinWorkerThreadFactory fac = null; UncaughtExceptionHandler handler = null; try { // ignore exceptions in accessing/parsing properties
30-09-2021

[~dl] I can't find anywhere that we specify the default parallelism of the common pool. From the spec one might guess default parallelism is availableProcessors() , NOT availableProcessors() - 1 I agree with [~dholmes] that max(1, NCPUs-1) is a reasonable choice. We do document that setting java.util.concurrent.ForkJoinPool.common.parallelism to zero is a "warranty-voiding operation" - but that's not happening here. It's very reasonable to expect the common pool to always provide at least one thread to run queued tasks. This feels like a regression from jdk16 that should be fixed.
29-09-2021

Shouldn't the default be max(1, NCPUs-1)? Setting ActiveProcessorCount is just a simple way to replicate the issue, the real issue is single CPU containers.
28-09-2021

Actually, there doesn't seem to be anything that could or should be changed. The current behavior is as specified, but is surprising due to how -XX:ActiveProcessorCount=1 now works. There isn't any way to guess the intent of users in the given test case without requiring use of -Djava.util.concurrent.ForkJoinPool.common.parallelism. I suppose we could discuss changing the spec if people are routinely surprised.
27-09-2021

Single CPU computers are becoming rare, but not non-existent. Java based software should work well on Raspberry Pi Zero and in single-cpu containers.
27-09-2021

It is a known and documented limitation that unjoined tasks may not complete when java.util.concurrent.ForkJoinPool.common.parallelism is zero. Setting -XX:ActiveProcessorCount=1 now has this effect, because the default is NCPUs-1 It seems that we should check for explicit setting parallelism to zero versus keeping at one due to ActiveProcessorCount.
27-09-2021

Likely related to JDK-8246585 - ForkJoin Updates Paging [~martin] and [~dl]
27-09-2021

The observations on Windows 10: JDK 17ea+4: Passed, "Hello World" was printed JDK 17ea+5: Failed, "Hello World" was not printed JDK 18ea+1: Failed.
27-09-2021