Thread Pool In Java

Within the Java programming language, a thread pool refers to a collection of pre-initialized, recyclable worker threads that remain in a state of readiness to execute tasks. The management of this thread pool falls under the responsibility of the Java Virtual Machine (JVM). Rather than generating a new thread for every task, these tasks are sent to the pool for processing by the existing threads. The utilization of a thread pool leads to enhanced performance, decreased overhead, and more effective resource utilization.

To read more How to Create Thread in Java

There are the following three conditions when a task is submitted:

  • If a thread is free, it executes the task immediately.
  • If all the threads are busy, the task waits in a queue until a thread becomes available.
  • After completing a task, the thread returns to the pool, not terminated.
  • Why use thread pool?

  • Performance: It avoids the cost of creating/destroying threads repeatedly.
  • Resource Management: It limits the number of concurrent threads, preventing system overload.
  • Scalability: It efficiently handles large numbers of short-lived tasks.
  • Executer Thread Pool Methods

Creating a thread pool of a fixed size can be done using the newFixedThreadPool(int s) method.

The newCachedThreadPool method generates a fresh thread pool that spawns new threads on demand while utilizing existing threads whenever they are accessible for use.

The method newSingleThreadExecutor instantiates a fresh thread.

Advantages of Java Thread Pool

  • The main advantage of thread pool is efficient thread management.
  • It reduces the overhead of creating and destroying threads, improves application performance, and provides better control over concurrency.
  • Threads are pre-created and reused, avoiding the costly process of repeatedly creating and destroying threads.
  • It leads to faster execution of short-lived tasks.
  • Limits the number of concurrent threads, preventing excessive memory and CPU usage.
  • It helps to avoid issues like thread starvation or system crashes due to uncontrolled thread creation.
  • Developers can define the maximum number of threads in the pool.
  • Ensures predictable behavior under heavy load by queuing tasks when all threads are busy.
  • After completing a task, a thread does not die; it returns to the pool ready for the next task.
  • Thread pools prevent applications from being overwhelmed by too many threads.
  • They provide a safer way to handle large numbers of tasks without risking system instability.
  • By balancing workload across a fixed number of threads, applications achieve higher throughput and responsiveness.
  • Example: Java Thread Pool

An illustration of Java thread pool can be demonstrated through the usage of ExecutorService and Executors.

Example

class WorkerThread implements Runnable {  

    private String message;  

    public WorkerThread(String s){  

        this.message=s;  

    }  

     public void run() {  

        System.out.println(Thread.currentThread().getName()+" (Start) message = "+message);  

        processmessage();//call processmessage method that sleeps the thread for 2 seconds  

        System.out.println(Thread.currentThread().getName()+" (End)");//prints thread name  

    }  

    private void processmessage() {  

        try {  Thread.sleep(2000);  } catch (InterruptedException e) { e.printStackTrace(); }    }  

}

File Name: Main.java

Example

import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;

public class Main {

	public static void main(String[] args) {

		ExecutorService executor = Executors.newFixedThreadPool(5);// creating a pool of 5 threads

		for (int i = 0; i < 10; i++) {

			Runnable worker = new WorkerThread("" + i);

			executor.execute(worker);// calling execute method of ExecutorService

		}

		executor.shutdown();

		while (!executor.isTerminated()) {

		}

		System.out.println("Finished all threads");

	}

}

Output:

Output

pool-1-thread-1 (Start) message = 0

pool-1-thread-2 (Start) message = 1

pool-1-thread-3 (Start) message = 2

pool-1-thread-5 (Start) message = 4

pool-1-thread-4 (Start) message = 3

pool-1-thread-2 (End)

pool-1-thread-2 (Start) message = 5

pool-1-thread-1 (End)

pool-1-thread-1 (Start) message = 6

pool-1-thread-3 (End)

pool-1-thread-3 (Start) message = 7

pool-1-thread-4 (End)

pool-1-thread-4 (Start) message = 8

pool-1-thread-5 (End)

pool-1-thread-5 (Start) message = 9

pool-1-thread-2 (End)

pool-1-thread-1 (End)

pool-1-thread-4 (End)

pool-1-thread-3 (End)

pool-1-thread-5 (End)

Finished all threads

Example: Thread Pool

Example

Example

import java.util.Date;

import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;

import java.text.SimpleDateFormat;

class Tasks implements Runnable {

	private String taskName;

// constructor of the class Tasks  

	public Tasks(String str) {

// initializing the field taskName   

		taskName = str;

	}

// Printing the task name and then sleeps for 1 sec  

// The complete process is getting repeated five times  

	public void run() {

		try {

			for (int j = 0; j <= 5; j++) {

				if (j == 0) {

					Date dt = new Date();

					SimpleDateFormat sdf = new SimpleDateFormat("hh : mm : ss");

//prints the initialization time for every task  

					System.out.println("Initialization time for the task name: " + taskName + " = " + sdf.format(dt));

				} else {

					Date dt = new Date();

					SimpleDateFormat sdf = new SimpleDateFormat("hh : mm : ss");

// prints the execution time for every task  

					System.out.println("Time of execution for the task name: " + taskName + " = " + sdf.format(dt));

				}

// 1000ms = 1 sec  

				Thread.sleep(1000);

			}

			System.out.println(taskName + " is complete.");

		} catch (InterruptedException ie) {

			ie.printStackTrace();

		}

	}

}

public class Main {

// Maximum number of threads in the thread pool  

	static final int MAX_TH = 3;

// main method  

	public static void main(String argvs[]) {

// Creating five new tasks  

		Runnable rb1 = new Tasks("task 1");

		Runnable rb2 = new Tasks("task 2");

		Runnable rb3 = new Tasks("task 3");

		Runnable rb4 = new Tasks("task 4");

		Runnable rb5 = new Tasks("task 5");

// creating a thread pool with MAX_TH number of  

// threads size the pool size is fixed  

		ExecutorService pl = Executors.newFixedThreadPool(MAX_TH);

// passes the Task objects to the pool to execute (Step 3)  

		pl.execute(rb1);

		pl.execute(rb2);

		pl.execute(rb3);

		pl.execute(rb4);

		pl.execute(rb5);

// pool is shutdown  

		pl.shutdown();

	}

}

Output:

Output

Initialization time for the task name: task 1 = 12 : 14 : 02

Initialization time for the task name: task 3 = 12 : 14 : 02

Initialization time for the task name: task 2 = 12 : 14 : 02

Time of execution for the task name: task 1 = 12 : 14 : 04

Time of execution for the task name: task 3 = 12 : 14 : 04

Time of execution for the task name: task 2 = 12 : 14 : 04

Time of execution for the task name: task 1 = 12 : 14 : 05

Time of execution for the task name: task 3 = 12 : 14 : 05

Time of execution for the task name: task 2 = 12 : 14 : 05

Time of execution for the task name: task 1 = 12 : 14 : 06

Time of execution for the task name: task 3 = 12 : 14 : 06

Time of execution for the task name: task 2 = 12 : 14 : 06

Time of execution for the task name: task 1 = 12 : 14 : 07

Time of execution for the task name: task 3 = 12 : 14 : 07

Time of execution for the task name: task 2 = 12 : 14 : 07

Time of execution for the task name: task 1 = 12 : 14 : 08

Time of execution for the task name: task 3 = 12 : 14 : 08

Time of execution for the task name: task 2 = 12 : 14 : 08

task 1 is complete.

task 3 is complete.

Initialization time for the task name: task 4 = 12 : 14 : 09

Initialization time for the task name: task 5 = 12 : 14 : 09

task 2 is complete.

Time of execution for the task name: task 4 = 12 : 14 : 10

Time of execution for the task name: task 5 = 12 : 14 : 10

Time of execution for the task name: task 4 = 12 : 14 : 11

Time of execution for the task name: task 5 = 12 : 14 : 11

Time of execution for the task name: task 4 = 12 : 14 : 12

Time of execution for the task name: task 5 = 12 : 14 : 12

Time of execution for the task name: task 4 = 12 : 14 : 13

Time of execution for the task name: task 5 = 12 : 14 : 13

Time of execution for the task name: task 4 = 12 : 14 : 14

Time of execution for the task name: task 5 = 12 : 14 : 14

task 4 is complete.

task 5 is complete.

By examining the program's output, it is clear that tasks 4 and 5 are carried out solely when there is an available thread. Prior to that, any additional tasks are placed in a queue.

In the scenario described, if there is a need to handle 50 tasks without creating individual threads for each task, a solution is to establish a thread pool with a capacity of 10 threads. This approach involves assigning 10 tasks to the available threads while placing the remaining 40 tasks in a queue. As any of the 10 threads becomes available after completing a task, it will proceed to pick up the next task from the queue. This process continues until all 50 tasks have been processed accordingly.

Risks and Considerations in Thread Pools

Below are the potential risks associated with utilizing thread pools:

Deadlock can occur in any program that utilizes multithreading, including scenarios involving a thread pool, which introduces an additional potential for deadlock. For instance, imagine a situation where all active threads are waiting for results from blocked threads in the queue, which are unable to proceed due to a lack of available threads for execution.

Thread Leakage: Thread leakage happens when a thread is taken from the pool to perform a task but fails to return to it upon task completion. An instance of this is when a thread throws an exception that the pool class cannot handle, causing the thread to exit and decrease the thread pool size by one. If this scenario repeats frequently, it can lead to the pool running out of threads, leaving no resources for handling additional requests.

Resource thrashing occurs when a significant amount of time is squandered on switching contexts between threads in situations where the thread pool size is extensive. If the number of threads surpasses the ideal quantity, it can result in a starvation issue, ultimately causing resource thrashing.

Important Points to Remember

  • Do not queue the tasks that are concurrently waiting for the results obtained from the other tasks. It may lead to a deadlock situation, as explained above.
  • Care must be taken whenever threads are used for the operation that is long-lived. It may result in the waiting of thread forever and will finally lead to the leakage of the resource.
  • The thread pool has to be ended explicitly. If it does not happen, then the program continues to execute, and it never ends. Invoke the shutdown method on the thread pool to terminate the executor. Note that if someone tries to send another task to the executor after shutdown, it throws a RejectedExecutionException.
  • To reduce the probability of running JVM out of memory, one can control the maximum running threads in JVM. The thread pool does not create new threads after it has reached the maximum limit.
  • Tuning the Thread Pool

The appropriate size of a thread pool relies on the quantity of processors available and the nature of tasks to be performed by the threads. In a scenario where a system possesses P processors dedicated solely to computational tasks, the optimal thread pool size for achieving peak efficiency is either P or P + 1. Nevertheless, if the tasks entail waiting for input/output operations, it becomes crucial to factor in the ratio of wait time (W) to service time (S) in order to determine the ideal pool size, which is P * (1 + W / S) for optimal efficiency.

Conclusion

Utilizing a thread pool is highly beneficial for effectively handling concurrency, particularly in server applications. Although the concept of a thread pool is straightforward, there are several considerations to address when working with it due to the associated risks.

Java Thread Pool MCQs

  1. What does a Java Thread Pool represent?
  • A single thread executing multiple tasks
  • A group of worker threads reused for tasks
  • Threads created for each task
  • Threads waiting indefinitely

Explanation: A thread pool maintains a fixed group of reusable worker threads to improve performance and resource use.

  1. Which method creates a fixed-size thread pool?
  • newCachedThreadPool
  • newSingleThreadExecutor
  • newFixedThreadPool(int s)
  • newWorkStealingPool

Explanation: Executors.newFixedThreadPool(int s) creates a thread pool with a constant number of threads.

  1. What happens when all threads in a pool are busy?
  • New threads are created automatically
  • Tasks are queued until a thread is free
  • Tasks are discarded
  • An exception is thrown

Explanation: Extra tasks are placed in a waiting queue until an available thread picks them up.

  1. Which method shuts down the thread pool?
  • stop
  • terminate
  • shutdown
  • end

Explanation: The shutdown method stops accepting new tasks and ends all running threads gracefully.

  1. What is a major advantage of thread pools?
  • More memory usage
  • Slower execution
  • Better performance
  • Random thread scheduling

Thread pools are designed to recycle threads, thus avoiding the overhead of creating and destroying them frequently.

Input Required

This code uses input(). Please provide values below: