JavaData & JavaUsing the Executor Framework to Deal with Java Threads

Using the Executor Framework to Deal with Java Threads

Threads provide a multitasking ability to a process (process = program in execution). A program can have multiple threads; each of them provide a unit of control as one of its strands. Single threaded programs execute in a monotonous, predictable manner. But, a multi-threaded program brings out the essence of concurrency or simultaneous execution of program instruction where a subset of code executes or is supposed to execute in parallel mode. This mechanism leverages performance, especially because modern processing workhorses are multi core. So, running a single threaded process that may utilize only one CPU core is simply a waste of resources.

Java core’s APIs includes a framework called Executors Framework, which provides some relief to the programmer when working in a multi-threaded arena. This article mainly focuses on the framework and its uses with a little background idea to begin with.

Parallel Execution

Parallel execution requires some hardware assistance, and a threaded program that brings out the essence of parallel processing is no exception. Multi-threaded programs can best utilize multiple CPU cores found in modern machines, resulting in manifold performance boost. But, the problem is that maximum utilization of multiple cores requires a program’s code to be written with parallel logic from the ground up. Practically, this is easier said than done. In dealing with simultaneous operations where everything is seemingly multiple, problems and challenges are also multi-faceted. Some logics are meant to be parallel whereas some are very linear. The biggest problem is to balance between them yet keep up with maximal utilization of processing resources. Parallel logic is inherently parallel, whose implementation is pretty straightforward, but converting a semi-linear logic into an optimal parallel code can be a daunting task. For example, the solution of 2 + 2 = 4 is quite linear but the logic to solve expression such as (2 x 4) + (5 / 2) can be leveraged with parallel implementation.

Parallel computing and concurrency, though closely related, are yet distinct. This article uses both words to mean same thing to keep it simple.

Refer to https://en.wikipedia.org/wiki/Parallel_computing to get a more elaborate idea on this.

Implementation Primitive

There are many aspects to be considered before modeling a program for multi-threaded implementation. Some basic questions to ask while modeling one are:

  • How is the thread to be created and submitted for execution?
  • Is dependency involved in the thread for successful execution?
  • Does it imply synchronous or asynchronous execution?
  • How do you find out whether the error cropped up from a thread in execution?
  • Who executes the thread?
  • How to get the feedback from the thread after execution is complete?

When creating a task (task = individual unit of work), what we normally do is either implement an interface called Runnable or extend the Thread class:

public class SampleTask implements Runnable {
   //...
   public void run(){
      //...
   }
}

And, create the task as follows:

SampleTask st1=new SampleTask();
SampleTask st2=new SampleTask();

And then execute each task as follows:

Thread t1=new Thread(st1);
Thread t2=new Thread(st2);

t1.start();
t2.start();

To get a feedback from individual task, we have to write additional code. But, the point is that there are too many intricacies involved in managing a thread execution, such as creation and destruction of a thread, has a direct bearing on the overall time required to start another task. If it is not performed gracefully, unnecessary delay in the start of a task is certain. A thread consumes resources, so multiple threads may consume multiple resources. This has a propensity to slack overall CPU performance; worse, it can crash the system if the number of threads exceeds the permitted limit of the underlying platform. It also may happen that some thread consumes most of the resources leaving other threads starved, or a typical race condition. So, the complexity involved in managing thread execution is easily intelligible.

Executor Framework

The Executor Framework attempts to address this problem and bring some controlling attributes. The predominant aspect of this framework is to state a clear demarcation between the task submission from task execution. The executor says, create your task and submit it to me; I’ll take care of the rest (execution details). The mechanics of this demarcation is attributed to the interface called Executor under the java.util.concurrent package. Rather than creating thread explicitly, the code above can be written as:

Executor executor=Executors.newFixedThreadPool(5);

and then

executor.execute(new SampleTask());   // or executor.execute(st1);
executor.execute(new SampleTask());   // or executor.execute(st2);

Calling the executor method does not ensure that the thread execution is initiated; instead, it merely refers to a submission of a task. The executor takes up the responsibility on behalf, including the details about the policies to adhere to in the course of execution. The class library supplied by the executor framework determines the policy, which, however, is configurable.

There are many static methods available with the Executors class (Note that Executor is an interface and Executors is a class. Both included in the package java.util.concurrent). A few of the commonly used are as follows:

  • newCachedThreadPool(): It creates new thread as needed, but is quick to reuse any available thread previously constructed. The thread pool can shrink and expand, depending upon the workload.
  • newFixedThreadPool(int nThreads): The thread pool created by this method has a fixed size set by the parameter passed. At any given time, there can be maximum of nThread (number of threads) in the pool.
  • newSingleThreadExecutor(): This method ensures that there will be only one thread to execute all the tasks. If this thread dies unexpectedly, a new one is created. But, there is a guarantee that there will be a single thread at any given time.

All of these methods return an ExecutorService object.

The ExecutorService interface extends Executor and provides necessary methods to manage execution of threads, such as the shutdown () method to initiate an orderly shutdown of threads. There is another interface, called ScheduledExecutorService, which extends ExecutorService to support scheduling of threads.

Refer to Java Documentation for more details on these methods and other service details. Note that the use of executor is highly customizable and one can be written from scratch.

A Quick Example

Let’s create a very simple program to understand the use of an executor.

package org.mano.example;

public class MyTask implements Runnable {

   private int id;
   private int counter;

   public MyTask(int id, int counter) {
      this.id = id;
      this.counter = counter;
   }

   @Override
   public void run() {
      for (int i = 0; i < counter; i++) {
         try {
            System.out.println("Task ID: " +
               id + " Iter No: " + i);
            Thread.sleep(1000);
         } catch (Exception ex) {
            System.out.println("Task ID: " +
               id + " is interrupted.");
            break;
         }
      }
   }
}

package org.mano.example;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TestMyTask {

   public static void main(String[] args) {
      ExecutorService executorService =
         Executors.newFixedThreadPool(2);
      for (int i = 0; i < 3; i++)
         executorService.submit(new MyTask(i, 5));
      executorService.shutdown();
   }
}

Conclusion

The Executor Framework is one of much assistance provided by the Java core APIs, especially in dealing with concurrent execution or creating a multi-threaded application. Some other assisting libraries useful for concurrent programming are explicit locks, synchronizer, atomic variables, and fork/join framework, apart from the executor framework. The separation of task submission from task execution is the greatest advantage of this framework. Developers can leverage this to reduce many of the complexities involved in executing multiple threads.

References

Get the Free Newsletter!
Subscribe to Developer Insider for top news, trends & analysis
This email address is invalid.
Get the Free Newsletter!
Subscribe to Developer Insider for top news, trends & analysis
This email address is invalid.

Latest Posts

Related Stories