Executing Tasks in Threads
We can consider a task as an independent activity that doesn’t depend on the result or side effect of other tasks. So, if you have sufficient resources (CPU, memory etc.), tasks can be executed in parallel. So should we always create individual threads for individual tasks to optimize the performance? The answer is no. When there are more runnable threads than available processors, threads sit idle. If you have too many idle threads, they will –
- Put pressure on the CPU.
- Consume a lot of memory and put pressure on the Garbage Collector.
- Combination of above two will degrade the performance.
So, a reasonable approach would be to always put some limit on how many threads your application should create based on the available CPU. To achieve that, we can use the thread pool
provided by the Java Executor
framework.
Thread Pool
A thread pool is a group of threads initially created that waits for tasks and executes them once available. So, the tread pool always makes a group of thread always available, so that we won’t have to waste time to create them every time when needed. So, you see, other than putting a limit on the number of threads, thread pool saves the thread creation time by reusing existing threads.
We’ll use a fixed‐size thread pool provided by the Executor framework that reuses a fixed number of threads.
public class Executors {
public static ExecutorService newFixedThreadPool(int nThreads) { ... }
}
Let’s consider a fixed size thread pool having a total number of threads 3.
ExecutorService executor = Executors.newFixedThreadPool(3);
The ExecutorService
provides the tread pool and methods to assign tasks to the thread pool. Also, if the number of tasks is more than the available threads in the pool, ExecutorService provides the facility to queue up tasks until there is a free thread available.
So in our example, a thread pool will be created with the active thread count 3. Now, let’s assume you have submitted 7 tasks.
As 3 threads are available in the thread pool, those threads will pick the first 3 tasks and remaining 4 tasks will be in queue.
Once a thread is done with the corresponding task, that thread will pick the next available task. This process will go one like this until all tasks are executed.
To put this in code, first create Runnable
task –
public class Task implements Runnable {
private final String taskName;
public Task(String taskName) {
this.taskName = taskName;
}
public void run() {
try {
System.out.println(LocalTime.now() + " : Running - " + taskName);
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Now, submit all the tasks to the thread pool.
public class ThreadTester {
public static void main(String[] args) throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(3);
executor.submit(new Task("Task 1"));
executor.submit(new Task("Task 2"));
executor.submit(new Task("Task 3"));
executor.submit(new Task("Task 4"));
executor.submit(new Task("Task 5"));
executor.submit(new Task("Task 6"));
executor.submit(new Task("Task 7"));
}
}
Output:
18:05:16.758 : Running - Task 2
18:05:16.758 : Running - Task 3
18:05:16.758 : Running - Task 1
18:05:21.758 : Running - Task 4
18:05:21.760 : Running - Task 5
18:05:21.760 : Running - Task 6
18:05:26.766 : Running - Task 7
If you want to shut down the ExecutorService, you can call the shutDown()
method. If you do that, ExecutorService will not accept new tasks, but can process the tasks which are already added in the Queue.
executor.shutdown();
ExecutorService, Runnable and Callable
As we said earlier, the ExecutorService
provides the thread pool and methods to assign tasks to the thread pool. Also, if the number of tasks is more than the available threads in the pool, ExecutorService provides the facility to queue up tasks until there is a free thread available.
In our example we have seen, Runnable
instances can be executed by ExecutorService if you submit that instance by calling ExecutorService.submit()
method.
Runnable cannot return a value. It will just execute whatever is defined inside the run()
method.
public interface Runnable {
void run();
}
If you want to return a value after executing a task, you have to use Callable
.
public interface Callable<V> {
V call() throws Exception;
}
Here V
represents the type of result.
You can submit both Callable
and Runnable
to ExecutorService and it will return a Future
.
public interface ExecutorService {
<T> Future<T> submit(Callable<T> task);
Future<?> submit(Runnable task);
}
You may ask, why is the submit()
method returning Future when the run()
method of Runnable doesn’t return anything? We shall discuss this, but before that let’s understand the Future class.
Future
Future represents the future result of an asynchronous task. When you submit a task to ExecutorService, the ExecutorService will take care of running the task, but it immediately returns a Future
object.
This future object can be used to get the result returned by the task. You may ask, the Future object is returned immediately as soon as we submit the task and the task may take some time to complete the execution. Then how can a future object give us the result as there is a possibility that the task is not finished yet?
The answer is, to get the result from Future, we call the Future.get()
method. This method will block until the task is complete. Only when the task is finished, this get()
method will provide the result and the execution will proceed.
You can use the Future.isDone()
method to check if the task is finished. If the task is finished, it will return true; otherwise, it returns false.
To test this, first create a Callable class. Let’s assume this Callable class is responsible for providing the square of a number and it takes 5 seconds.
public class Task implements Callable<Integer> {
private final int input;
public Task(int input) {
this.input = input;
}
@Override
public Integer call() throws Exception {
System.out.println(LocalTime.now() + " : Calculating square...");
Thread.sleep(5000);
return input*input;
}
}
Now submit this Callable to ExecutorService and wait for the result.
public class ThreadTester {
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(3);
Future<Integer> future = executor.submit(new Task(5));
System.out.println(LocalTime.now() + " : Waiting for result...");
int result = future.get();
System.out.println(LocalTime.now() + " : Result : " + result);
executor.shutdown();
}
}
Output:
16:40:02.599 : Calculating square...
16:40:02.600 : Waiting for result...
16:40:07.601 : Result : 25
As you can see from the output –
- As soon as we submit the task,
Future
is returned by the ExecutorService. Future.get()
blocked the main thread till the callable is finished.- Once the
Callable
is done (after 5 seconds), the block is cleared and the result is printed.
Why is the submit() method returning a Future for Runnable task when the run() method returns nothing?
The reason is pretty simple. The returned Future
object can be used to check if the Runnable
has finished. Otherwise you won’t have any option to check if the Runnable is done.
To test this, first create a Runnable
class. Let’s assume the run()
method will take 5 seconds.
public class Task implements Runnable {
@Override
public void run() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Now submit this Callable to ExecutorService and wait for the result.
public class ThreadTester {
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(3);
Future<?> future = executor.submit(new Task());
System.out.println(LocalTime.now() + " : Waiting for result...");
Object result = future.get();
System.out.println(LocalTime.now() + " : Result : " + result);
executor.shutdown();
}
}
Output:
16:52:04.726 : Waiting for result...
16:52:09.619 : Result : null
As you can see from the output –
- As soon as we submit the task,
Future
is returned by the ExecutorService. Future.get()
blocked the main thread till the Runnable is finished.- Once the
Runnable
is done (after 5 seconds), the block is cleared andnull
is returned as result.
Sometimes, when you deal with a Runnable
task, you don’t want to wait for the task completion. You just want to submit the task and go ahead. To achieve that, do not call the FFuture.get()
method or instead of ExecutorService.submit()
method, use ExecutorService.execute()
method. This method accepts a Runnable
instance and doesn’t return anything.
executor.execute(new Task());
That’s it for now. Hope you have enjoyed this tutorial. If you have any doubt, please ask in the comment section. I will try to answer that as soon as possible. Till then, bye bye.