Java's Fork/Join vs ExecutorService - when to use which?

Fork-join allows you to easily execute divide and conquer jobs, which have to be implemented manually if you want to execute it in ExecutorService. In practice ExecutorService is usually used to process many independent requests (aka transaction) concurrently, and fork-join when you want to accelerate one coherent job.


Fork-join is particularly good for recursive problems, where a task involves running subtasks and then processing their results. (This is typically called "divide and conquer" ... but that doesn't reveal the essential characteristics.)

If you try to solve a recursive problem like this using conventional threading (e.g. via an ExecutorService) you end up with threads tied up waiting for other threads to deliver results to them.

On the other hand, if the problem doesn't have those characteristics, there is no real benefit from using fork-join.


References:

  • Java Tutorials: Fork/Join.
  • Java Tip: When to use ForkJoinPool vs ExecutorService:

Java 8 provides one more API in Executors

static ExecutorService  newWorkStealingPool()

Creates a work-stealing thread pool using all available processors as its target parallelism level.

With addition of this API,Executors provides different types of ExecutorService options.

Depending on your requirement, you can choose one of them or you can look out for ThreadPoolExecutor which provides better control on Bounded Task Queue Size, RejectedExecutionHandler mechanisms.

  1. static ExecutorService newFixedThreadPool(int nThreads)

    Creates a thread pool that reuses a fixed number of threads operating off a shared unbounded queue.

  2. static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)

    Creates a thread pool that can schedule commands to run after a given delay, or to execute periodically.

  3. static ExecutorService newCachedThreadPool(ThreadFactory threadFactory)

    Creates a thread pool that creates new threads as needed, but will reuse previously constructed threads when they are available, and uses the provided ThreadFactory to create new threads when needed.

  4. static ExecutorService newWorkStealingPool(int parallelism)

    Creates a thread pool that maintains enough threads to support the given parallelism level, and may use multiple queues to reduce contention.

Each of these APIs are targeted to fulfil respective business needs of your application. Which one to use will depend on your use case requirement.

e.g.

  1. If you want to process all submitted tasks in order of arrival, just use newFixedThreadPool(1)

  2. If you want to optimize performance of big computation of recursive tasks, use ForkJoinPool or newWorkStealingPool

  3. If you want to execute some tasks periodically or at certain time in future, use newScheduledThreadPool

Have a look at one more nice article by PeterLawrey on ExecutorService use cases.

Related SE question:

java Fork/Join pool, ExecutorService and CountDownLatch