7

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?

Ravindra babu
  • 37,698
  • 11
  • 250
  • 211
Gili
  • 86,244
  • 97
  • 390
  • 689
  • There is a much better performing random for release mode https://docs.oracle.com/javase/tutorial/essential/concurrency/threadlocalrandom.html – zapl Dec 01 '15 at 03: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. – Gili Dec 01 '15 at 04:31
  • Have you tried setting parallelism to 0? – pvg Dec 01 '15 at 05:19
  • @pvg Yes. The Javadoc states that this is illegal and the code throws an exception if you try. – Gili Dec 01 '15 at 05:23
  • @Gili it does? What JDK are you using? The docs/impl notes I'm looking at say things like "It is possible to disable or limit the use of threads in the common pool by setting the parallelism property to zero, and/or using a factory that may return null" for JDK 8. – pvg Dec 01 '15 at 05:29
  • 1
    @pvg I am running JDK 1.8.0_66. According to http://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ForkJoinPool.html#ForkJoinPool-int- `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. – Gili Dec 01 '15 at 05:38
  • http://stackoverflow.com/questions/10797568/what-determines-the-number-of-threads-a-java-forkjoinpool-creates – Sotirios Delimanolis Dec 03 '15 at 15:37

2 Answers2

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;
    }
};
Gili
  • 86,244
  • 97
  • 390
  • 689
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 ).

tuxdna
  • 8,257
  • 4
  • 43
  • 61
  • 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. – Gili Dec 01 '15 at 05:22
  • Could you update your question with a small code that demonstrates your issue? – tuxdna Dec 02 '15 at 09:56