Concurrency in Java: Executor Service (Part 2)

Abhinav Singh
Geek Culture
Published in
5 min readMay 22, 2022

This is in continuation to:
Concurrency in Java: Executor Service (Part 1)

Memory Model in Java

The Java memory model used internally in the JVM divides memory between thread stacks and the heap. Each thread running in the Java virtual machine has its own thread stack. The thread call stack contains information about the history of methods called by the thread to reach the current point of execution. As the thread executes its code, the call stack changes.

The thread call stack also contains the list of local variables being used up in execution. Each thread has it’s own call stack, and cannot access the memory of other thread’s stack. So even if two different threads are executing same block of code, there will be two copies of local variables being created in each thread call stack.

JVM Memory Management

If the local variable is of a primitive type, it’s stored entirely within the stack. If not then the variable is stored in the heap and the reference is stored in the call stack. Static class variables are also stored on the heap along with the class definition. If two threads call a method on the same object at the same time, they will both have access to the object’s member variables, but each thread will have its own copy of the local variables.

JVM can utilize two kinds of memory:

  • Reserved — the size which is guaranteed to be available by a host’s OS (but still not allocated and cannot be accessed by JVM) — it’s just a promise
  • Committed — already taken, accessible, and allocated by JVM

Callable Interface

There are two ways to create threads in Java, either by extending the Thread class or by implementing the Runnable interface. However, one feature missing in Runnable class is that the run() method cannot return us a value i.e. it has a return type null. Callable solves for this issue.

There are two basic differences between Callable and Runnable interface:

  • To implement a Runnable interface you have to implement the run() method, which has a return type of null. To implement a Callable interface you need to implement the call() method, which can return a result.
  • run() method cannot throw an exception. This feature is also available in call() method of Callable interface.
public Object call() throws Exception;

Future

Similar to Runnable, Callable is an interface that encapsulates tasks meant to be run on different threads. But another problem arises with Callable, which is to store results that arrive after being processed asynchronously. Future interface solves for this problem. Future is used to store a result obtained from a different thread, and can be used with both Runnable and Callable tasks.

A Future represents the result of an asynchronous computation. Methods are provided to check if the computation is complete, to wait for its completion, and to retrieve the result of the computation. The result can only be retrieved using method get when the computation has completed, blocking if necessary until it is ready. Cancellation is performed by the cancel method. Additional methods are provided to determine if the task completed normally or was cancelled. Once a computation has completed, the computation cannot be cancelled. If you would like to use a Future for the sake of cancellability but not provide a usable result, you can declare types of the form Future<?> and return null as a result of the underlying task.

boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;

Note that we have two implementations of the get() method. The first one will block the execution and wait for indefinite time to get the result, while the other one will wait for a specific time before cancelling the task execution.

FutureTask: Future for Runnable Interface

The FutureTask class is an implementation of Future that implements Runnable, and so may be executed by an Executor. For example, the above construction with submit could be replaced by:

FutureTask<String> future = new FutureTask<>(task);
executor.execute(future);

Because FutureTask implements Runnable, a FutureTask can be submitted to an Executor for execution.

Assigning Tasks to the ExecutorService

ExecutorService can execute Runnable and Callable tasks. We can assign tasks to the ExecutorService using several methods including execute(), which is inherited from the Executor interface, and also submit(), invokeAny() and invokeAll().

The execute() method executes the given command at some time in the future. The command may execute in a new thread, in a pooled thread, or in the calling thread, at the discretion of the Executor implementation.It has a return type void and doesn’t give any possibility to get the result of a task’s execution or to check the task’s status (is it running):

executorService.execute(runnableTask);

submit() submits a Callable or a Runnable task to an ExecutorService and returns a result of type Future. It submits a value-returning task for execution and returns a Future representing the pending results of the task. The Future’s get method will return the task’s result upon successful completion.:

Future<String> future = 
executorService.submit(callableTask);

invokeAny() assigns a collection of tasks to an ExecutorService, causing each to run, and returns the result of a successful execution of one task (if there was a successful execution). Executes the given tasks, returning the result of one that has completed successfully (i.e., without throwing an exception), if any do before the given timeout elapses. Upon normal or exceptional return, tasks that have not completed are cancelled. But this method comes with an added complexity. The results of this method are undefined if the given collection is modified while this operation is in progress.:

String result = executorService.invokeAny(callableTasks);

invokeAll() assigns a collection of tasks to an ExecutorService, causing each to run, and returns the result of all task executions in the form of a list of objects of type Future. It is used in cases where multiple tasks needs to be executed at once. It executes the given tasks, returning a list of Futures holding their status and results when all complete. Future.isDone is true for each element of the returned list. The results of this method are also undefined if the given collection is modified while this operation is in progress.:

List<Future<String>> futures = executorService.invokeAll(callableTasks);

Conclusion

Java Executor Service hides a lot of complexity but also makes it easy for you to dive in and tweak the inner workings if you so choose. The Executors class provides a lot of factory methods that address different use cases.

It gives the developer the ability to control the number of generated threads and the granularity of tasks that should be run by separate threads. The best use case for ExecutorService is the processing of independent tasks, such as transactions or requests according to the scheme “one thread for one task.”

Congratulations on making it to the end! Feel free to talk about tech or any cool projects on Twitter, GitHub, Medium, LinkedIn, or Instagram.

Thanks for reading!

--

--