Java Thread Pool Example

Active threads consume system resources, which can cause JVM creating too many threads which means that the system will quickly run out of memory.

That’s the problem Thread Pool in Java helps solving.

How Thread Pools work?

Thread Pool reuses previously created threads for current tasks. That solves the problem of the need of too many threads so running out of memory is not an option. You can even think of Thread Pool as a recycle system. And not only does it eliminate the option of running out of memory, it also makes the application respond very quickly since there is a thread already existing when the request arrives.

Thread Pool Java

The workflow of the diagram above, allows you to control the number of threads an application is making and also it lets you control the schedule tasks’ execution and keep incoming tasks in a queue.




Executors, Executor and ExecutorService

Java provides the Executor framework which means you only need to implement the Runnable objects and send them to the executor to execute.

To use thread pools, firstly we need to create an object of ExecutorService and pass tasks to it. ThreadPoolExecutor class sets the core and maximum pool size. The runnables are then executed sequentially.

Different Executor Thread Pool methods

newFixedThreadPool(int size) - creates a fixed size thread pool

newCachedThreadPool() - creates a thread pool that creates new threads if needed but will also use previous threads if they are available

newSingleThreadExecutor() - creates a single thread

ExecutorService interface contains many methods that are used to control the progress of tasks and manages the termination of the service. You can control tasks’ execution using the Future instance. An example of how to use Future:

ExecutorService execService = Executors.newFixedThreadPool(6);
Future<String> future = execService.submit(() -> "Example");
String result = future.get();

ThreadPoolExecutor lets you implement an extensible thread pool that has a lot of parameters,  which are corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler, threadFactor. However, corePoolSize, maximumPoolSize and keepAliveTime are the main ones as they are used in every constructor.

corePoolSize is the number of threads to keep in the pool, even if they are idle, unless allowCoreThreadTimeOut is set.

maximumPoolSize is the maximum number of threads that are allowed in the pool.

keepAliveTime is 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.

For more information about the other parameters, visit the original Oracle documentation.

Thread Pool implementation example

Workflow steps:

  1. create a task to execute
  2. create Executor Pool by using Executors
  3. pass tasks to the Executor Pool
  4. shutdown the Executor Pool

Task.java

import java.text.SimpleDateFormat;  
import java.util.Date; 
import java.util.concurrent.ExecutorService; 
import java.util.concurrent.Executors; 

// (Step 1) 
public class Task implements Runnable    { 
    private String name; 
      
    public Task(String name) { 
        this.name = name; 
    } 
      
    public void run() { 
        try {
            for (int i = 0; i < 5; i++) { 
                if (i == 1) { 
                    Date date = new Date(); 
                    SimpleDateFormat ft = new SimpleDateFormat("hh:mm:ss"); 
                    System.out.println("Time initialization for task " + this.name + " is " + ft.format(date));    
                } 
                else { 
                    Date date = new Date(); 
                    SimpleDateFormat ft = new SimpleDateFormat("hh:mm:ss"); 
                    System.out.println("Execution time for task " + this.name + " is " + ft.format(date));    
                } 
                Thread.sleep(1000); 
            } 
        } 
        catch(InterruptedException error) { 
            error.printStackTrace(); 
        } 
        System.out.println(this.name + " completed"); 
    } 
} 

Main.java

import java.text.SimpleDateFormat;  
import java.util.Date; 
import java.util.concurrent.ExecutorService; 
import java.util.concurrent.Executors; 

public class Main { 
    public static void main(String[] args) { 
        Runnable task1 = new Task("task 1"); 
        Runnable task2 = new Task("task 2"); 
        Runnable task3 = new Task("task 3"); 
        Runnable task4 = new Task("task 4"); 
        Runnable task5 = new Task("task 5");       
          
        // (Step 2) 
        ExecutorService pool = Executors.newFixedThreadPool(3);   
         
        // (Step 3) 
        pool.execute(task1); 
        pool.execute(task2); 
        pool.execute(task3); 
        pool.execute(task4); 
        pool.execute(task5);  
          
        // (Step 4) 
        pool.shutdown();     
    } 
} 
  

Output:

Time initialization for task task 2 is 10:18:40
Time initialization for task task 1 is 10:18:40
Time initialization for task task 3 is 10:18:40
Execution time for task task 3 is 10:18:41
Execution time for task task 1 is 10:18:41
Execution time for task task 2 is 10:18:41
Execution time for task task 2 is 10:18:42
Execution time for task task 3 is 10:18:42
Execution time for task task 1 is 10:18:42
Execution time for task task 1 is 10:18:43
Execution time for task task 3 is 10:18:43
Execution time for task task 2 is 10:18:43
Execution time for task task 3 is 10:18:44
Execution time for task task 1 is 10:18:44
Execution time for task task 2 is 10:18:44
task 2 completed
task 1 completed
task 3 completed
Time initialization for task task 4 is 10:18:45
Time initialization for task task 5 is 10:18:45
Execution time for task task 4 is 10:18:46
Execution time for task task 5 is 10:18:46
Execution time for task task 4 is 10:18:47
Execution time for task task 5 is 10:18:47
Execution time for task task 5 is 10:18:48
Execution time for task task 4 is 10:18:48
Execution time for task task 4 is 10:18:49
Execution time for task task 5 is 10:18:49
task 4 completed
task 5 completed

Breakdown of the code implementation above:

Task.java represents the task class. Each task has a name instance variable and each task is instantiated by the use of a constructor. This class has 1 method, which is called run. Within the body of the run method, there is a for loop that iterates by the number of tasks there are. In our case, there are 5 tasks, which means it will run 5 times. The first iteration, print the time the current task was initialized. The other iterations, print the execution time. After it has been printed, there is a Thread.sleep() method call which is used to display every iteration message with 1 second delay. Note that the method name “run” is important to be called like that as it is an abstract method coming from Runnable which our Task class is implementing.

Task 4 and 5 are only executed when a tread in the pool becomes idle. Until then, the extra tasks are placed in a queue.

After all tasks have been executed, shutdown the Thread Pool.

When is thread pool useful

When organizing server applications. As stated in the beginning of the article, it is useful when organizing server applications as using thread pool is very efficient as if there are many tasks, it automatically places them in a queue. Not only that, but it also prevents running out of memory, or at least it slows significantly the process of doing so. Using ExecutorService makes it easier to implement.

 

Leave a Reply

avatar