Writing multithreaded code and expecting it to work as expected has been a moment of joy as well as a nightmare both at the same time in the earlier releases of Java.
Prior to Java 5, the way we used to create Threads were :
- Extend the Thread class
- Implement the Runnable interface
Let us go over the following code snippet:
package com.kapil.concurrency;
public class SimpleThread {
public static void main(String[] args) throws InterruptedException {
System.out.println(" The current thread name is " + Thread.currentThread().getName());
Thread thread = new Thread( new Runnable() {
@Override
public void run() {
System.out.println(" I'm " + Thread.currentThread().getName());
System.out.println(" I simply execute code. I do not return anything ");
}
});
thread.setName(" Thread 1");
thread.start();
System.out.println("I'm going to sleep . Let me sleep atleast 2 seconds ");
Thread.currentThread().sleep(2000);
System.out.println(" That was a nice sleep");
System.out.println(" Happy Learning !. Bye !");
}
}
In the above sample program, we are creating a Thread named “Thread 1” by using an anonymous object and starting it from the main Thread. Even though this piece of code isn’t doing much it still looks verbose. Imagine us having so much of complex logic and what if we had to start let’s say many such threads. There definitely was scope for improvement.
Java 5 introduced major changes wrt how we dealt with multi-threading and concurrency. The java.util.concurrent package offered so many exciting new features to Java. Let us take a look at one of the major features which was the ExecutorService.
As we saw earlier, the earlier approach was manageable for fewer number of threads, but as the number of threads grew; the verbosity and complexity grew as well. As our application grows we would want to manage only the business logic(what goes inside the Runnable’s run() method) and let some other class/entity handle the thread management for us. Well ExecutorService to the rescue.
Java 5 introduced the ExecutorService to help us to create and manage threads and their lifecycle. Let us understand how Executor Service works by referencing the following diagram:

ExecutorService enables us to submit tasks to be run in a separate thread without us bothering to create thread for each task and starting the threads individually. It provides powerful methods like execute() and submit() that lets us run the task (the Runnable/Callable code) that we want to execute and handles the thread creation and management itself.
Let us consider the following code snippet and look into further details:
package com.kapil.concurrency;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class ExecutorServiceExample {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService executorService = Executors.newFixedThreadPool(5);
System.out.println(" I'm the main thread. You don't believe. Check this out " + Thread.currentThread().getName());
// let us execute a runnable task
executorService.execute(() -> {
System.out.println("Hello There ! I'm " + Thread.currentThread().getName());
});
// let us execute a Callable task
Future<Employee> future = executorService.submit(() -> {
Thread.currentThread().sleep(3000);
System.out.println();
return new Employee(1, "Anand", "Kumar");
});
//Thread.currentThread().sleep(2000);
Employee employee = future.get();
System.out.println(employee);
executorService.shutdown(); // do in finally block
}
}
In the above example, we are executing 2 different types of task.
In Line number 10, we are creating an ExecutorService with a fixed Thread Pool of 5. We can see the same in the previous diagram.
In Line number 14, we are invoking the execute() and passing it the Runnable task that we want to execute. (Using lambda here)
Similarly in line number 19, we are invoking the submit() and passing it the Callable task that we want to execute.
These tasks get added to the Blocking Queue as shown in above diagram and are picked up by the poll of 5 threads depending on their availability. Thus we see we did not have to explicitly start any threads and all the thread management was taken care by the ExecutorService.
ExecutorService offers 2 methods to terminate the executor:
- shutdown() -> Initiates an orderly shutdown in which previously submitted tasks are executed (inluding those present in the Blocking queue), but no new tasks will be accepted.
- shutdownNow() –> Attempts to stop all actively executing tasks, and returns the list of the tasks that were awaiting execution in the Blocking queue
So, this was one of the 4 different types of ExecutorService available to us. The complete list is as stated below. We can go over these individually in a separate thread.
- Fixed Thread Pool Executor
- Cached Thread Pool Executor
- Single Thread Executor
- Schedule Thread Pool Executor
So that’s it for this post. Go ahead and run this piece of code in your favorite java editor.
See you until next time. Happy Learning !