Can someone explain me why some tasks sometimes run in the main thread although they were submitted to the common ForkJoinPool before?
Here is an example in Java 16 (adopt-openjdk-16.0.1):
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Future;
public class StrangeUseOfThreads {
public static void main(String[] args) throws ExecutionException, InterruptedException {
int mainThreadCounter = 0;
int forkJoinPoolThreadCounter = 0;
for (int i = 0; i < 100; i++) {
Future<String> future = ForkJoinPool.commonPool().submit(() -> Thread.currentThread().getName());
String threadName = future.get();
if(threadName.equals("main")) {
mainThreadCounter++;
} else {
forkJoinPoolThreadCounter++;
}
}
System.out.println("mainThreadCounter=" + mainThreadCounter);
System.out.println("forkJoinPoolThreadCounter=" + forkJoinPoolThreadCounter);
System.out.println("------------------------------------");
mainThreadCounter = 0;
forkJoinPoolThreadCounter = 0;
for (int i = 0; i < 100; i++) {
Future<String> future = ForkJoinPool.commonPool().submit(() -> Thread.currentThread().getName());
ForkJoinPool.commonPool().submit(Thread::yield); // let's disturb here
String threadName = future.get();
if(threadName.equals("main")) {
mainThreadCounter++;
} else {
forkJoinPoolThreadCounter++;
}
}
System.out.println("mainThreadCounter=" + mainThreadCounter);
System.out.println("forkJoinPoolThreadCounter=" + forkJoinPoolThreadCounter);
}
}
That's the output for me:
mainThreadCounter=80
forkJoinPoolThreadCounter=20
------------------------------------
mainThreadCounter=0
forkJoinPoolThreadCounter=100
While the main thread is often used in the first loop, it is not used at all in the second loop because I "disturbed" the sequence of
Future<String> future = ForkJoinPool.commonPool().submit(() -> Thread.currentThread().getName());
String threadName = future.get();
by adding
ForkJoinPool.commonPool().submit(Thread::yield);
between those two lines which is, of course, somehow useless but it cannot obviously removed by the dead code elimination optimization of the JIT compiler, I guess.
I suspect the JIT compiler optimizes here something with the ForkJoinPool because the get()-call on the future follows directly after the submission of the task. However, I have desperately googled for such an optimzation and have not found any.
What's going on here "under the hood"?
EDIT1:
tgdavies inspired me to modify the example to get stack traces.
That's my example now (it's ugly but it works!):
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Future;
public class StrangeUseOfThreads {
public static void main(String[] args) throws ExecutionException, InterruptedException {
int mainThreadCounter = 0;
int forkJoinPoolThreadCounter = 0;
for (int i = 0; i < 100; i++) {
Future<Throwable> future = ForkJoinPool.commonPool().submit(
() -> new Throwable(Thread.currentThread().getName()));
Throwable throwable = future.get();
if(throwable.getMessage().equals("main")) {
if(mainThreadCounter==0) {
throwable.printStackTrace();
}
mainThreadCounter++;
} else {
if(forkJoinPoolThreadCounter==0) {
throwable.printStackTrace();
}
forkJoinPoolThreadCounter++;
}
}
System.out.println("mainThreadCounter=" + mainThreadCounter);
System.out.println("forkJoinPoolThreadCounter=" + forkJoinPoolThreadCounter);
}
}
That's the output now:
java.lang.Throwable: main
at net.mirwaldt.completable.futures.examples.StrangeUseOfThreads.lambda$main$0(StrangeUseOfThreads.java:13)
at java.base/java.util.concurrent.ForkJoinTask$AdaptedCallable.exec(ForkJoinTask.java:1458)
at java.base/java.util.concurrent.ForkJoinTask.doExec$$$capture(ForkJoinTask.java:295)
at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java)
at java.base/java.util.concurrent.ForkJoinTask.tryExternalHelp(ForkJoinTask.java:386)
at java.base/java.util.concurrent.ForkJoinTask.externalInterruptibleAwaitDone(ForkJoinTask.java:356)
at java.base/java.util.concurrent.ForkJoinTask.get(ForkJoinTask.java:1009)
at net.mirwaldt.completable.futures.examples.StrangeUseOfThreads.main(StrangeUseOfThreads.java:14)
java.lang.Throwable: ForkJoinPool.commonPool-worker-19
at net.mirwaldt.completable.futures.examples.StrangeUseOfThreads.lambda$main$0(StrangeUseOfThreads.java:13)
at java.base/java.util.concurrent.ForkJoinTask$AdaptedCallable.exec(ForkJoinTask.java:1458)
at java.base/java.util.concurrent.ForkJoinTask.doExec$$$capture(ForkJoinTask.java:295)
at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java)
at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1016)
at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1665)
at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1598)
at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:183)
mainThreadCounter=91
forkJoinPoolThreadCounter=9
That's interesting:
While the task in the main thread was effectively triggered by the get()-call on the future, the task in the ForkJoinPool thread was submitted to the ForkJoinPool.
However, I still don't know why. I can't imagine that's a bug.