Q. What is a thread pool, and how will you create them in Java? Why do you need an Executor framework? A. A thread pool is a collection of runnables with a work queue. The threads in the pool constantly run and check the work queue for new work. If there is new work to be done they execute this Runnable. In Java 5, Executor framework was introduced with the java.util.concurrent.Executor interface. This was introduced to fix some of the shortcomings discussed below. 1. The Executor framework is a framework for standardizing invocation, scheduling, execution, and control of asynchronous tasks according to a set of execution policies. |
2. Even though the threads are light-weighted than creating a process, creating them utilizes a lot of resources. Also, creating a new thread for each task will consume more stack memory as each thread will have its own stack and also the CPU will spend more time in context switching. Creating a lot many threads with no bounds to the maximum threshold can cause application to run out of heap memory. So, creating a ThreadPool is a better solution as a finite number of threads can be pooled and reused. The runnable or callable tasks will be placed in a queue, and the finite number of threads in the pool will take turns to process the tasks in the queue.
Here is the sample code:
import java.util.concurrent.ExecutorService;3. The Runnable interface's void run( ) method has no way of returning any result back to the main thread. The executor framework introduced the Callable interface that returns a value from its call( ) method. This means the asynchronous task will be able to return a value once it is done executing.
import java.util.concurrent.Executors;
public class Sum implements Runnable {
private static final int NO_OF_THREADS= 3;
int maxNumber;
public Sum(int maxNumber) {
this.maxNumber = maxNumber;
}
/** method where the thread execution will start **/
public void run(){
int sum = 0;
for (int i = 0; i = maxNumber; i++) {
sum += maxNumber;
}
System.out.println("Thread " + Thread.currentThread().getName() + " count is " + sum);
}
/** main thread. Always there by default. **/
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(NO_OF_THREADS); // create a pool of 3 threads
for (int i = 10000; i < 10100; i++) {
Runnable worker = new Sum(i); // create worker threads
executor.execute(worker); // add runnables to the work queue
}
// This will make the executor accept no new threads
// and finish all existing threads in the queue
executor.shutdown();
// Wait until all threads have completed
while (!executor.isTerminated()) {
}
System.out.println("Finished all threads");
}
}
import java.util.ArrayList;The output is something like
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Sum implements Callable<String> {
private static final int NO_OF_THREADS = 3;
int maxNumber;
public Sum(int maxNumber) {
this.maxNumber = maxNumber;
}
/** method where the thread execution will start
* this can return a value
*/
public String call(){
int sum = 0;
for (int i = 0; i <= maxNumber; i++) {
sum += maxNumber;
}
return Thread.currentThread().getName() + " count is " + sum;
}
/** main thread. Alwyas there by default. **/
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(NO_OF_THREADS); // create a pool of 3 threads
List<Future<String>> list = new ArrayList<Future<String>>(10); // provides facility to return results asynchronously
for (int i = 10000; i < 10100; i++) {
Callable<String> worker = new Sum(i); // create worker threads
Future<String> submit = executor.submit(worker); // add callables to the work queue
list.add(submit); // provides facility to return results asynchronously
}
//process the results asynchronously when each thread completes its task
for (Future<String> future : list) {
try {
System.out.println("Thread " + future.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
executor.shutdown();
System.out.println("Finished all threads");
}
}
Thread pool-1-thread-1 count is 100010000
Thread pool-1-thread-2 count is 100030002
Thread pool-1-thread-3 count is 100050006
Thread pool-1-thread-1 count is 100070012
Thread pool-1-thread-1 count is 100090020
...
4. The various Executor implementations provide different execution policies to be set while executing the tasks. For example, the ThreadPool supports the following policies:
- newFixedThreadPool: Creates threads as tasks are submitted, up to the maximum pool size, and then attempts to keep the pool size constant.
- newCachedThreadPool: Can add new threads when demand increases, no bounds on the size of the pool.
- newSingleThreadExecutor: Single worker thread to process tasks, Guarantees order of execution based on the queue policy (FIFO, LIFO, priority order).
- newScheduledThreadPool: Fixed-size, supports delayed and periodic task execution.
Q. What design pattern does the executor framework use?
A. The Executor is based on the producer-consumer design pattern, where threads that submit tasks are producers and the threads that execute tasks are consumers. In the above examples, the main thread is the producer as it loops through and submits tasks to the worker threads. The "Sum" (i.e. a worker thread) is the consumer that executes the tasks submitted by the main (i.e. consumer) thread. Check this blog to learn more detailed explanation on the producer-consumer design pattern.