Press "Enter" to skip to content

JDK9’s ForkJoinPool Upgrades

While everyone’s busy with modularity, local-variable-type-inference, and other Next Big Things of recent JDK releases, there’s a fairly small and important update for the ForkJoinPool that deserves some attention.

ForkJoinPool was an experiment brought to life by JDK 7 and attracted a lot of attention at that time – it’s main selling point was the implementation of the idea of work-stealing – simply put, free threads were able to steal tasks from worker queues of other busy threads within the same pool.

ForkJoinPool Configuration

Since the beginning, ForkJoinPool suffered from a lack of reasonable config options. The most generous constructor offered us only parameters such as:

  1. Parallelism level
  2. A custom ForkJoinWorkerThreadFactory
  3. A custom UncaughtExceptionHandler
  4. asyncMode
public ForkJoinPool(
  int parallelism,
  ForkJoinWorkerThreadFactory factory,
  UncaughtExceptionHandler handler,
  boolean asyncMode)

Some of you, that were a bit more intrusive, would discover that there’s one more private constructor available since JDK 8 which offers an additional, very useful parameter: the worker name prefix.

I must admit that it was very disappointing to see this one being private and not accessible using any legal means, but luckily there are other ways for achieving the same result.

JDK9

JDK9, however, brought a huge improvement – firstly, the implementation was rewritten using VarHandles, and we got a new, very generous constructor exposing additional configuration parameters such as:

public ForkJoinPool(
  // ...
  int corePoolSize,
  int maximumPoolSize,
  int minimumRunnable,
  Predicate<? super ForkJoinPool> saturate,
  long keepAliveTime, TimeUnit unit
)

Let’s see what do those give us.

int corePoolSize

This one is pretty self-explanatory:

The number of threads to keep in the pool.

Normally (and * by default) this is the same value as the parallelism level, * but may be set to a larger value to reduce dynamic overhead if * tasks regularly block.

Using a smaller value (for example 0) has the same effect as the default.

However, it’d be important to add that the maximum possible value is 32767.

int maximumPoolSize

Pretty self-explanatory as well. By default, 256 spare threads are allowed.

int minimumRunnable

It’s the first huge improvement that gives us an opportunity to ensure that there’s at least N usable threads in the pool – usable threads are those that aren’t blocked by a join() or a ManagedBlocker instance. When a number of free unblocked threads go below the provided value, new threads get spawned if maximumPoolSize allows it.

Setting the minimumRunnable to a larger value might ensure better throughput in the presence of blocking tasks for the cost of the increased overhead (remember to make sure that gains are bigger than costs).

If we know that our tasks won’t need any additional threads, we can go for 0.

Predicate<? super ForkJoinPool> saturate

If we end up in a situation when there’s an attempt made to spawn more threads in order to satisfy the minimumRunnable constraint, but it gets blocked by the maximumPoolSize, by default, RejectedExecutionException(“Thread limit exceeded replacing blocked worker”) is thrown.

But now, we can provide a Predicate that gets fired once such situation occurs, and eventually allow thread pool saturation by ignoring the minimumRunnable value.

It’s good to see that we have a choice now.

long keepAliveTime, TimeUnit unit

Just like with the classic ExecutorService, we can now specify how long unused threads should be kept alive before getting terminated.

Keep in mind that it applies only for threads spawned above the corePoolSize value.

Conclusion

JDK9 brought huge improvements for ForkJoinPool.

Unfortunately, we still can’t provide a custom worker name prefix easily, and cap the size of the worker queue, which is now capped at “1 << 24” – which is way too much than any reasonable value.




If you enjoyed the content, consider supporting the site: