Java Multi-threading and the Challenges of Parallel Computing
Parallel computing – the jargon, which is implicitly coupled with high speed and high performance computation, is one of the most discussed topics in today’s computer world. In very simple terms, parallel computing is to break the main task into smaller units and simultaneously execute them to achieve the results.
Traditionally in Java, parallel/concurrent programming has been considered to be one of the most difficult tasks to handle due to the overhead in managing threads. But since the release of Java5 and Java6, the specification has enhanced the multi-threading model with new APIs. Often programmers opt to use their own concurrency mechanisms (high level). So the idea is to offer standard and most efficient concurrency utilities that facilitate programmers to write a variety of multithreaded applications. The required classes and interfaces are made available in the java.util.concurrent package.
Multi-Threading Model in Java
Concurrency in Java has always been a language feature with the support of Thread class, Runnable interface, synchronized and volatile keywords, etc. The conventional use of Thread class involved creating a Runnable class and overriding the run () method. The run () method would be implicitly invoked using the start (), which is as shown below:
Since the multi-threading model in Java was not considered to be the best solution in achieving parallelism due to performance issues, Java5 introduced the new java.util.concurrency package to address concurrency issues. One of the major additions to this package is the Executor interface. The Executor interface is built on top of the Thread class and abstracts all of the thread pool management.
Executors help in separating the Thread creation and managing task from the actual thread implementation task. The same task of Thread creation with Executor can be achieved in the following way:
The ExecutorService interface is an implementation of the Executor interface and uses Thread Pools, which hold and manage several worker threads. A task is submitted to the Thread Pool via an internal worker queue, which will be assigned to a worker thread once it completes its previous task. By invoking the newFixedThreadPool() factory method of the Executor interface, a thread pool with a fixed size can be created, ensuring that there is always a fixed number of threads available for processing the tasks.
Illustration of the Example:
To explain the Thread model in Java we consider the following example of computing the sum of first n natural numbers. The same example has been used in the entire article in explaining the various concepts associated with the Thread model in Java. Mathematically solving this problem is quite simple, and can be achieved using the following formula:
While, the manual approach of evaluating the sum would involve finding the sum using this approach:
The problem can also be thought as computing the sum of an array, which has first n natural numbers. The problem is basically broken down using recursion since it would be helpful in explaining the Fork/Join process. The recursive method used in this approach breaks n into smaller values of n to compute the sum and evaluates them together to get the final sum value. The following recursive function is defined in EvaluateSum class.
This implementation breaks the value n into two distinct smaller halves and evaluates the sum of the smaller values. The breakdown stops when the startIndex and endIndex becomes equal, this is basically the stopping condition for the recursive function. This class also defines an interface method, solveProblem(), which can be invoked by other classes.
Executors and Runnable Threads:
The implementation of the Runnable thread is quite straightforward wherein, MyRunnable class implements the Runnable interface and overrides the run () method to invoke the solveProblem () to evaluate the sum, which acts as an interface for the computeSum() method, which evaluates the sum.
The implementation of the ExecutorService is described in the following snippet:
The client MyMain creates an instance of MyRunnable and passes it to the executor, which invokes the run() method implicitly to evaluate the sum. This example clearly explains that Executor interface and shows how Executor abstracts the thread creation strategy and handles them separately.
Executors and Callable Threads:
While working with Executors, the thread functionality is implemented by a Runnable object, which cannot return a value. In a few scenarios wherein the thread implementation needs to return values to the main thread, the thread class must implement the java.util.concurrent.Callable interface.
The following code snippet shows the definition of the Callable thread, which defines a generic call () method to invoke the solveProblem () method of the EvaluateSum class. Also, it uses the Generic syntax to define the return type of the call () method, which is Integer in this particular case.
The implementation for the Executor is as follows.
The Executor creates 2 Callable threads in order to evaluate the sum. The invokeAll() method accepts a collection of Callable instances and returns a collection of java.util.concurrent.Future objects and throws an InterruptedException. A Future instance corresponds to the results of an asynchronous computation and represents the future result of the computation. Future instance also specifies if the computation was complete or not.
The isDone() and isCancelled() methods of the Future instance are used to find out if the Future instance have completed their computation or not. The other important aspect to remember here is that the invokeAll() is blocking in nature. Once a thread starts executing the call(), the other Callable threads are blocked. It is always important to shut down the executor to ensure that the JVM exits when the client class finishes its processing as there would still be active threads in the Thread pool.
In the next installment, we will describe the Fork/Join framework and how it is used to address the parallelism issues.
The authors would like to sincerely thank Mr. Subrahmanya SV, VP, ECOM Research Group, E&R for the guidance, support and constant encouragement and Mr. Piram for kindly reviewing the article.
Nitin KL. Nitin works as a Technology Lead at Infosys Ltd. He has a very good knowledge of Java EE technologies. He is involved in design and development of Java EE applications using Hibernate, iBATIS and JPA. He has written many online articles for Devx. Nitin can be reached at KL_Nitin@infosys.com.
Sangeetha S. S Sangeetha works as a Senior Technology Architect at the E-Commerce Research Labs at Infosys Ltd. She has over 14 years of experience in design and development of Java and Java EE applications. She has co-authored a book on ‘J2EE Architecture’ and also has written numerous online articles on Java for JavaWorld, java.net, Devx. She can be reached at firstname.lastname@example.org.