Introduction

Normally, when you implement a simple, concurrent Java application, you implement some Runnable objects and then the corresponding Thread objects. You control the creation, execution, and status of those threads in your program. Java 5 introduced an improvement with the Executor and ExecutorService interfaces and the classes that implement them (for example, the ThreadPoolExecutor class).

The Executor framework separates the task creation and its execution. With it, you only have to implement the Runnable objects and use an Executor object. You send the Runnable tasks to the executor and it creates, manages, and finalizes the necessary threads to execute those tasks.

Java 7 goes a step further and includes an additional implementation of the ExecutorService interface oriented to a specific kind of problem. It's the fork/join framework.

This framework is designed to solve problems that can be broken into smaller tasks using the divide and conquer technique. Inside a task, you check the size of the problem you want to resolve, and if it's bigger than an established size, you divide it into smaller tasks that are executed using the framework. If the size of the problem is smaller than the established size, you solve the problem directly in the task, and then, optionally, it returns a result. The following diagram summarizes this concept:

There is no formula to determine the reference size of a problem that determines if a task is to be subdivided or not, depending on its characteristics. You can use the number of elements to process in the task and an estimation of the execution time to determine the reference size. Test different reference sizes to choose the best one for your problem. You can consider ForkJoinPool as a special kind of Executor.

The framework is based on the following two operations:

  • Fork operation: When you divide a task into smaller tasks and execute them using the framework.
  • Join operation: When a task waits for the finalization of the tasks it has created. It's used to combine the results of those tasks.

The main difference between the fork/join and the Executor frameworks is the work-stealing algorithm. Unlike the Executor framework, when a task is waiting for the finalization of the subtasks it has created using the join operation, the thread that is executing that task (called worker thread) looks for other tasks that have not been executed yet and begins their execution. In this way, the threads take full advantage of their running time, thereby improving the performance of the application.

To achieve this goal, the tasks executed by the fork/join framework have the following limitations:

  • Tasks can only use the fork() and join() operations as synchronization mechanisms. If they use other synchronization mechanisms, the worker threads can't execute other tasks when they are in the synchronization operation. For example, if you put a task to sleep in the fork/join framework, the worker thread that is executing that task won't execute another one during the sleeping time.
  • Tasks should not perform I/O operations such as read or write data in a file.
  • Tasks can't throw checked exceptions. They have to include the code necessary to process them.

The core of the fork/join framework is formed by the following two classes:

  • ForkJoinPool: This class implements the ExecutorService interface and the work-stealing algorithm. It manages the worker threads and offers information about the status of the tasks and their execution.
  • ForkJoinTask: This is the base class of the tasks that will execute in the ForkJoinPool. It provides the mechanisms to execute the fork() and join() operations inside a task and the methods to control the status of the tasks. Usually, to implement your fork/join tasks, you will implement a subclass of three subclasses of this class: RecursiveAction for tasks with no return result, RecursiveTask for tasks that return one result, and CountedCompleter for tasks that launch a completion action when all the subtasks have finished.

Most of the features provided by this framework were included in Java 7, but Java 8 included minor features in it. It included a default ForkJoinPool object. You can obtain it using the static method, commonPool(), of the ForkJoinPool class. This default fork/join executor will by default use the number of threads determined by the available processors of your computer. You can change this default behavior by changing the value of the system property, java.util.concurrent.ForkJoinPool.common.parallelism. This default pool is used internally by other classes of the Concurrency API. For example, Parallel Streams use it. Java 8 also included the CountedCompleter class mentioned earlier.

This chapter presents five recipes that show you how to work efficiently with the fork/join framework.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.141.104.73