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.
- 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.
Why use thread pool?
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.
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
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:
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
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:
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
- 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.
- 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.
- 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.
- 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.
- 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.