ADDITIONAL SYSTEM INFORMATION :
Reproduced on Linux and Windows.
Is present in Java 17.0.5 and 18.0.2, but not in 16.0.1 and 19.0.1.
A DESCRIPTION OF THE PROBLEM :
After switching to Java 17 we noticed that our applications, using a fork join pool, had more threads running in parallel than set by the parallelism value. This behavior can be observed when submitting a sub task from an internal task and waiting on the result of the sub task with ForkJoinTask.get(long, TimeUnit). It seems that if the fork join pool has enough tasks submitted or the tasks take long enough to execute the fork join pool will start additional threads and exceed the parallelism value. The precise conditions for this to happen are unclear. The same behavior does not show when waiting on tasks with the blocking ForkJoinTask.get().
REGRESSION : Last worked in version 16
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
See the code example
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
Expected the fork join pool to not have more threads running concurrently than the parallelism value, which is the behavior in Java 16 & 19 or when using the blocking ForkJoinTask.get().
---------- BEGIN SOURCE ----------
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.TimeUnit;
public class ForkJoinParallelism {
static final ForkJoinPool pool = new ForkJoinPool(2);
public static void main(final String[] args) {
final Timer timer = new Timer();
final Statistics statistics = new Statistics();
timer.schedule(statistics, 100, 100);
try {
final List<ForkJoinTask<?>> tasks = new ArrayList<>();
for (int c = 0; c < 3; c++) {
System.out.println("start cycle #" + c);
for (int i = 0; i < 50; i++) {
tasks.add(pool.submit(() -> {
final List<ForkJoinTask<?>> subtasks = new ArrayList<>();
for (int j = 0; j < 50; j++) {
subtasks.add(pool.submit(() -> {
// spend some cpu time
final Random rnd = new Random();
final int[] numbers = new int[100_000];
for (int k = 0; k < 100_000; k++) {
numbers[k] = rnd.nextInt(100_000);
}
Arrays.sort(numbers);
}));
}
getAll(subtasks);
}));
}
getAll(tasks);
System.out.println("end cycle #" + c);
}
} catch (final Exception e) {
e.printStackTrace();
} finally {
timer.cancel();
pool.shutdownNow();
System.out.println("parallelism: " + pool.getParallelism());
System.out.println("max running threads: " + statistics.max);
}
}
static void getAll(final List<ForkJoinTask<?>> tasks) {
for (final ForkJoinTask<?> task : tasks) {
try {
// using the blocking get removes the behavior
// task.get();
task.get(20, TimeUnit.SECONDS);
} catch (final Exception e) {
e.printStackTrace();
}
}
}
public static class Statistics extends TimerTask {
int max = 0;
@Override
public void run() {
final int running = ForkJoinParallelism.pool.getRunningThreadCount();
System.out.println("running thread count: " + running);
if (running > max) {
max = running;
}
}
}
}
---------- END SOURCE ----------
FREQUENCY : always