Concurrency 2
Concurrency 2
Executor Framework
Overview
Using Executor Framework
Thread Pools
Types of Thread Pool
Benefits of Executor Framework
Coding Problems
Adder - Subtractor
Overview
Imagine you have a computer program that needs to do several tasks at the same time. For
example, your program might need to download files, process data, and update a user
interface simultaneously. In the traditional way of programming, you might use threads to
handle these tasks. However, managing threads manually can be complex and error-prone.
Java ExecutorService implementations let you stay focused on tasks that need to be run, rather
than thread creation and management.
Think of a chef in a kitchen as your program. The chef has multiple tasks like chopping
vegetables, cooking pasta, and baking a cake. Instead of the chef doing each task one by one,
the chef hires sous-chefs (threads) to help. The chef (Executor Framework) can assign tasks to
sous-chefs efficiently, ensuring that multiple tasks are happening simultaneously, and the
kitchen operates smoothly.
In Java, the Executor Framework provides a convenient way to implement this idea in your
code, making it more readable, maintainable, and efficient. It simplifies the process of
managing tasks concurrently, so you can focus on solving the problems your program is
designed to address.
Now, let's look at a real-world example to illustrate the use of the Executor Framework.
Consider a scenario where you have a set of tasks that need to be executed concurrently to
improve performance. We'll use a ThreadPoolExecutor for this example:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
Here NumberPrinter() is runnable task as created earlier. The Executor interface is used to
execute tasks. It is a generic interface that can be used to execute any kind of task. The
Executor interface has only one method:
The execute method takes a Runnable object as a parameter. The Runnable interface is a
functional interface that has only one method. Executors internally use a thread pool to
execute the tasks. The execute method is non-blocking. It returns immediately after submitting
the task to the thread pool. The execute method is used to execute tasks that do not return a
result. A thread pool is a collection of threads that are used to execute tasks. Instead of
creating a new thread for each task, a thread pool reuses the existing threads to execute the
tasks. This improves the performance of the application.
Creating threads, destroying threads, and then creating them again can be expensive.
A thread pool mitigates the cost, by keeping a set of threads around, in a pool, for
current and future work.
Threads, once they complete one task, can then be reassigned to another task, without
the expense of destroying that thread and creating a new one.
1. Worker Threads are available in a pool to execute tasks. They're pre-created and kept
alive, throughout the lifetime of the application.
2. Submitted Tasks are placed in a First-In First-Out queue. Threads pop tasks from the
queue, and execute them, so they're executed in the order they're submitted.
3. The Thread Pool Manager allocates tasks to threads, and ensures proper thread
synchronization.
In Java, the ExecutorService interface, along with the ThreadPoolExecutor class, provides a
flexible thread pool framework. Here are five types of thread pools in Java, each with different
characteristics:
1. FixedThreadPool
Description: A thread pool with a fixed number of threads.
Characteristics:
Creation:
2. CachedThreadPool
Description: A thread pool that dynamically adjusts the number of threads based on
demand.
Characteristics
3. SingleThreadExecutor
Description: A thread pool with only one thread.
Characteristics:
4.ScheduledThreadPool
Description: A thread pool that supports scheduling of tasks.
Characteristics:
Similar to FixedThreadPool but with added support for scheduling tasks at fixed
rates or delays.
Suitable for periodic tasks or tasks that need to be executed after a certain
delay.
5. WorkStealingPool
Description: Introduced in Java 8, it's a parallelism-friendly thread pool.
Characteristics:
These different types of thread pools cater to various scenarios and workloads. The choice of a
thread pool depends on factors such as task characteristics, execution requirements, and
resource constraints in your application.
The Executor Framework makes it easier to execute tasks concurrently. It abstracts away the
low-level details of managing threads, so you don't have to worry about creating and
controlling them yourself.
When you have many tasks to perform, creating a new thread for each task can be inefficient.
The Executor Framework provides a pool of threads that can be reused for multiple tasks. This
reuse of threads reduces the overhead of creating and destroying threads for every task.
With the Executor Framework, you can control how many tasks can run simultaneously,
manage the lifecycle of threads, and specify different policies for task execution. This level of
control is important for optimizing the performance of your program.
4. Enhanced Scalability
When your program needs to handle more tasks, the Executor Framework makes it easier to
scale. You can adjust the size of the thread pool to accommodate more tasks without rewriting
a lot of code.
5. Task Scheduling
The framework allows you to schedule tasks to run at specific times or after certain intervals.
This is useful for scenarios where you want to automate repetitive tasks or execute tasks at
specific points in time.
Summary
The call method returns a result of type V. The call method can throw an exception. The
Callable interface is used to execute tasks that return a result.
For instance we can use the Callable interface to execute a task that returns the sum of two
numbers:
In order to execute a task that returns a result, we can use the submit method of the
ExecutorService interface. The submit method takes a Callable object as a parameter. The
submit method returns a Future object. The Future interface has a method called get that
returns the result of the task. The get method is a blocking method. It waits until the task is
completed and then returns the result of the task.
Futures can be used to cancel tasks. The Future interface has a method called cancel that can
be used to cancel a task. The cancel method takes a boolean parameter. If the boolean
parameter is true, the task is cancelled even if the task is already running. If the boolean
parameter is false, the task is cancelled only if the task is not running.
import java.util.ArrayList;
import java.util.concurrent.Callable;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
}
@Override
public List<Integer> call() throws Exception {
//Business Logic
//base case
if(arr.size()<=1){
return arr;
}
//recursive case
int n = arr.size();
int mid = n/2;
leftArr = leftFuture.get();
rightArr = rightFuture.get();
//Merge
List<Integer> output = new ArrayList<>();
int i=0;
int j=0;
while(i<leftArr.size() && j<rightArr.size()){
if(leftArr.get(i)<rightArr.get(j)){
output.add(leftArr.get(i));
i++;
}
else{
output.add(rightArr.get(j));
j++;
}
}
// copy the remaining elements
while(i<leftArr.size()){
output.add(leftArr.get(i));
i++;
}
while(j<rightArr.size()){
output.add(rightArr.get(j));
j++;
}
return output;
}
}
Main.java
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class DownloadManager {
private ExecutorService executorService;
Solution
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
@Override
public void run() {
// Simulate file download
System.out.println("Downloading file from: " + fileUrl);
class DownloadManager {
private ExecutorService executorService;
downloadManager.downloadFiles(filesToDownload);
downloadManager.shutdown();
}
}
Solution
Repainting a 2D array using four threads can be achieved by dividing the array into quadrants,
and assigning each quadrant to a separate thread for repainting.
This example divides the 2D array into four quadrants and assigns each quadrant to a separate
thread for repainting. The ArrayRepainterTask class represents the task for repainting a specific
quadrant. The program then uses an ExecutorService with a fixed thread pool to concurrently
execute the tasks. Finally, it prints the repainted 2D array.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Override
public void run() {
// Simulate repainting for the specified quadrant
for (int i = startRow; i <= endRow; i++) {
for (int j = startCol; j <= endCol; j++) {
array[i][j] = array[i][j] * 2; // Repaint by doubling the
values (simulated)
}
}
}
}
Synchronisation
Whenever we have multiple threads that access the same resource, we need to make sure that
the threads do not interfere with each other. This is called synchronisation.
Synchronisation can be seen in the adder and subtractor example. The adder and subtractor
threads access the same counter variable. If the adder and subtractor threads do not
synchronise, the counter variable can be in an inconsistent state.
Adder
package addersubtractor;
public class Adder implements Runnable {
private Count count;
public Adder (Count count) {
this.count = count;
}
@Override
public void run() {
for (int i = 1 ; i <= 100; ++ 1) {
count.value += i;
}
}
Subtracter
package addersubtractor;
public class Subtractor implements Runnable {
private Count count;
public Subtractor (Count count) {
this.count = count
}
@Override
public void run() {
for (int i = 1 ; i <= 100; ++ 1) {
count.value -= i;
}
}
Count class
package addersubtractor;
public class Count {
int value = 0;
}
system.out.println(count.value);