8

Is it possible to configure ForkJoinPool to use 1 execution thread?

I am executing code that invokes Random inside a ForkJoinPool. Every time it runs, I end up with different runtime behavior, making it difficult to investigate regressions.

I would like the codebase to offer "debug" and "release" modes. "debug" mode would configure Random with a fixed seed, and ForkJoinPool with a single execution thread. "release" mode would use system-provided Random seeds and use the default number of ForkJoinPool threads.

I tried configuring ForkJoinPool with a parallelism of 1, but it uses 2 threads (main and a second worker thread). Any ideas?

7
  • There is a much better performing random for release mode docs.oracle.com/javase/tutorial/essential/concurrency/… Commented Dec 1, 2015 at 3:39
  • @zapl I already use ThreadLocalRandom for release mode. This question isn't about improving performance. It is about improving ease-of-debugging by configuring ForkJoinPool to use a single thread. Commented Dec 1, 2015 at 4:31
  • Have you tried setting parallelism to 0? Commented Dec 1, 2015 at 5:19
  • @pvg Yes. The Javadoc states that this is illegal and the code throws an exception if you try. Commented Dec 1, 2015 at 5:23
  • 1
    @pvg I am running JDK 1.8.0_66. According to docs.oracle.com/javase/8/docs/api/java/util/concurrent/… parallelism may not be zero. I see the sentence you are referring to, but (1) I want to configure the number of threads used by a new ForkJoinPool instance, not the common instance. (2) If you dig into the JDK source-code you will discover that setting the aforementioned property to zero will result in the aforementioned exception. In short, this won't work. Commented Dec 1, 2015 at 5:38

2 Answers 2

8

So, it turns out I was wrong.

When you configure a ForkJoinPool with parallelism set to 1, only one thread executes the tasks. The main thread is blocked on ForkJoin.get(). It doesn't actually execute any tasks.

That said, it turns out that it is really tricky providing deterministic behavior. Here are some of the problems I had to correct:

  • ForkJoinPool was executing tasks using different worker threads (with different names) if the worker thread became idle long enough. For example, if the main thread got suspended on a debugging breakpoint, the worker thread would become idle and shut down. When I would resume execution, ForkJoinThread would spin up a new worker thread with a different name. To solve this, I had to provide a custom ForkJoinWorkerThreadFactory implementation that ensures only one thread runs at a time, and that its name is hard-coded. I also had ensure that my code was returning the same Random instance even if a worker thread shut down and came back again.
  • Collections with non-deterministic iteration order such as HashMap or HashSet led to elements grabbing random numbers in a different order on every run. I corrected this by using LinkedHashMap and LinkedHashSet.
  • Objects with non-deterministic hashCode() implementations, such as Enum.hashCode(). I forget what problems this caused but I corrected it by calculating the hashCode() myself instead of relying on the built-in method.

Here is a sample implementation of ForkJoinWorkerThreadFactory:

class MyForkJoinWorkerThread extends ForkJoinWorkerThread { MyForkJoinWorkerThread(ForkJoinPool pool) { super(pool); // Change thread name after ForkJoinPool.registerWorker() does the same setName("DETERMINISTIC_WORKER"); } } ForkJoinWorkerThreadFactory factory = new ForkJoinWorkerThreadFactory() { private WeakReference<Thread> currentWorker = new WeakReference<>(null); @Override public synchronized ForkJoinWorkerThread newThread(ForkJoinPool pool) { // If the pool already has a live thread, wait for it to shut down. Thread thread = currentWorker.get(); if (thread != null && thread.isAlive()) { try { thread.join(); } catch (InterruptedException e) { log.error("", e); } } ForkJoinWorkerThread result = new MyForkJoinWorkerThread(pool); currentWorker = new WeakReference<>(result); return result; } }; 
Sign up to request clarification or add additional context in comments.

1 Comment

Good. It would be helpful for others if you post some important blocks of code related to ThreadFactory.
1

Main thread is always the first thread your application will create. So when you create a ForkJoinPool with parallelism of 1, you are creating another thread. Effectively there will be two threads in the application now ( because you created a pool of threads ).

If you need only one thread that is Main, you can execute your code in sequence ( and not in parallel at all ).

2 Comments

I already know this. To clarify, I need to be able to alter the number of threads used by ForkJoinPool without altering the rest of my code. Your suggestion to execute code in sequence (dumping the use of ForkJoinPool) implies a design-level change every time I wish to switch between "debug" and "release" modes.
Could you update your question with a small code that demonstrates your issue?

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.