Why do we need ThreadPools in Java?

We all know how to create Threads in Java.

  • Create an implementation of Runnable
  • Wrap it inside a Thread class and call the start() method.

Something like this below

         Runnable runnableTask = () -> {
             System.out.println("Job Started.");
             System.out.println("This is running in new thread : " + Thread.currentThread().getName());
             System.out.println("Job Completed.");
         };

         new Thread(runnableTask).start();


If we need more threads, we can create them in a loop.
So why don’t we just create threads on the run, and instead need a ThreadPool?

The answer lies in the fact that creating a Thread is expensive.
Secondly, if we create threads uncontrollably we may run out of these resources quickly.

Having a Threadpool has its advantages

  • Reduce Resource Consumption
    • Thread pools can reduce the consumption caused by thread creation and destruction by reusing created threads.
  • Improve Response Speed
    • When a task arrives, the task can be executed immediately without waiting for the thread to be created.

With a Threadpool, we can control the number of threads the application creates and the life cycle of the threads.


Understanding how Java thread pool works

Lets look at the image below

  • Applications can submit multiple tasks for execution
  • Each task goes into a task queue
  • There is a thread pool with “limited number of threads” and each thread picks up a tasks
  • Once all threads are busy, remaining tasks are in a “waiting” state – waiting for thread to get free.
  • As soon as a thread gets free, it picks up the next tasks in the queue.
  • Threads can also return results back on completion

One more illustration for the same

Image Courtesy: https://gompangs.tistory.com/entry/JAVA-ExecutorService-%EA%B4%80%EB%A0%A8-%EA%B3%B5%EB%B6%80

Executor, ExecutorService, ThreadPoolExecutor in Java

Lets first look at the class hierarchy to understand how these three classes relate to each other

Lets understand them one by one

Executor

  • An object that executes submitted Runnable tasks.
  • 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.
  • Has only a single method void execute(Runnable command);

ExecutorService

  • ExecutorService extends Executor, AutoCloseable
  • It is an Executor that provides methods to
    • manage termination and
    • methods that can produce a Future for tracking progress of one or more asynchronous tasks.
  • An ExecutorService can be shut down, which will cause it to reject new tasks.
  • Important methods of ExecutorService
    • submit – extends base method Executor. execute(Runnable) by creating and returning a Future that can be used to cancel execution and/ or wait for completion.
    • invokeAny and invokeAll – perform the most commonly useful forms of bulk execution, executing a collection of tasks and then waiting for at least one, or all, to complete.
    • shutdown – Initiates an orderly shutdown in which previously submitted tasks are executed, but no new tasks will be accepted.
    • shutdownNow – Attempts to stop all actively executing tasks, halts the processing of waiting tasks, and returns a list of the tasks that were awaiting execution.
    • awaitTermination – Blocks until all tasks have completed execution after a shutdown request, or the timeout occurs, or the current thread is interrupted, whichever happens first.
  • The Executors class provides factory methods for the executor services provided in this package.e.g. Executors. newFixedThreadPool(...) , etc.

ThreadPoolExecutor

  • ThreadPoolExecutor extends AbstractExecutorService
  • It is actually an ExecutorService that executes each submitted task using one of possibly several pooled threads, normally configured using Executors factory methods.
  • Thread pools address two different problems
    • they usually provide improved performance when executing large numbers of asynchronous tasks, due to reduced per-task invocation overhead
    • and they provide a means of bounding and managing the resources, including threads, consumed when executing a collection of tasks.
  • Each ThreadPoolExecutor also maintains some basic statistics, such as the number of completed tasks.
  • Important attributes of a ThreadPoolExecutor
    • Core and maximum pool sizes
      • corePoolSize – the number of threads to keep in the pool, even if they are idle, unless allowCoreThreadTimeOut is set.
      • maximumPoolSize – the maximum number of threads to allow in the pool.
      • keepAliveTime – when the number of threads is greater than the core, this is the maximum time that excess idle threads will wait for new tasks before terminating.
  • New threads are created using a ThreadFactory.
    • If not otherwise specified, a Executors. defaultThreadFactory is used, that creates threads to all be in the same ThreadGroup and with the same NORM_PRIORITY priority and non-daemon status.
    • By supplying a different ThreadFactory, you can alter the thread’s name, thread group, priority, daemon status, etc.
  • Queuing vs new thread creation
    • If fewer than corePoolSize threads are running, the Executor always prefers adding a new thread rather than queuing.
    • If corePoolSize or more threads are running, the Executor always prefers queuing a request rather than adding a new thread.
    • If a request cannot be queued, a new thread is created unless this would exceed maximumPoolSize, in which case, the task will be rejected.


In this blog we only covered theoretical concepts about Executors, and ExecutorService and ThreadPoolExecutor.

In future blogs we will cover different types of ThreadPoolExecutor, along with code examples.