The Traditional/Conventional way of handling asynchronous tasks with Thread class
When you develop a multi-threaded java application, you create a Runnable instance and corresponding Thread instance for executing them. This is the most common and preferred way of doing concurrent programming among most of the developers. Dealing with Thread class directly may lead to few issues and those will be described below.
The issues with dealing with Thread class directly.
The creation of new thread for each task is really expensive. This is because it involves few steps with managing the lifecycle of the Thread. It needs to be created, started and the entire lifecycle of the thread instance should be managed. This consumes more time and system resources. When the number of tasks are increasing/becoming high, the number of threads that need to be created will also be high.
In addition, working directly with Thread class can be error-prone. For instance, if some any error/exception occurs, it is hard to handle them with Thread class. This is because the run() method does not throw any exception. On the other hand, there is no way to check whether the thread has executed successfully because, run() method does not return anything (return type of run() method is void).
When the thread object is started, there is no way to control its behavior. Simply, there is no way to suspend its execution later. (If there is a situation to suspend the execution of some started threads, we cannot do it if we manually created and started threads). even if there are suspend() and stop() methods available in Thread class, those have been deprecated now.
Maintaining and managing a pool of threads that can be used to handle/execute the submitted tasks. The thread pool can be reused to handle the upcoming tasks without creating new threads for each request. This will help to reduce the number of threads created.
Find a way to throw exception and return the output after the execution. This is possible with Callable and Future. Callable supports throwing exception from run() method and returning output with Future interface.
A Better solution with ExecutorService
ExecutorService is a higher level replacement for working with threads directly. This is capable of executing tasks asynchronously (in background) with managed thread pool. The main advantage of ExecutorService over traditional Thread class is that,it maintains a pool of worker threads and re-use them for executing tasks (in the background). Once the execution of the task is completed, the worker thread will not go to dead state and it will come back to the pool for serving next available task. Therefore it eliminates the unnecessary cost and overhead associated with creating separate new thread for each task in traditional Thread class approach.
The ExecutorService has some useful methods which accept either Runnable or Callable instances as method parameters. The most of the methods support of returning the output that is wrapped by Future interface. Therefore the calling process(caller) can know the ultimate execution status/result of the background task.
The ExecutorService has some methods like shutDown() and shutDownNow(). Those methods will help to control/manage the ExecutorService with already submitted and running tasks.
What are the Executor, ExecutorService and Executors?
Executor is the top level interface and ExecutorService is an extended interface of it. Executors is an utility class which has few factory methods for creating different executor services with different types of thread pools.
Here is just a class and interface level representation.
ThreadPoolExecutor is almost similar to the ExecutorService.
What are the different types of executor services available?
ExecutorService with different Thread Pool implementations.
- Single Thread for handling all the tasks.
Please refer the below code example for Executors.newSingleThreadExecutor();
If you run the above program, you will notice that it will create just a single thread and reuse it for executing all the submitted callable objects.
- Fixed number of threads for handling all tasks
Executors.newFixedThreadPool(5); // This will create a thread pool with 5 threads.
Please refer the below code example for Executors.newFixedThreadPool(5);
If you run the above program, you will notice that there are only five threads in the thread pool. those threads will be reused to execute all the tasks. No additional threads will be created and the available five threads will be reused.
- Creating new threads as needed (based on the demand) and re-using the previously constructed threads when they are available.
Please refer the below code example for Executors.newCachedThreadPool();
Controlling the ExecutorService
shutdown() :- The ExecutorService will not accept any new tasks once the shutdown method is called. All the previous submitted tasks will be executed before shut downing the executorService.
shutdownNow() :- The ExecutorService will be shutdown immediately by terminating already running tasks. The tasks that are pending on execution will no be executed and will be returned back as a list. So it is guaranteed that calling this method will shut down the ExecutorService immediately.
Either Runnable or Callable instances (Single Object or List of Objects) will be accepted as method arguments by the execution related methods in the ExecutorService. If you want to access the currently running thread, you can get it by Thread.currentThread() and it will return the current worker thread.