ExecutorService Interface
ExecutorService is an interface in Java that allows the execution of asynchronous tasks in the background concurrently, based on the Replicated Workers model. For example, there might be a need to send a number of requests, but it would be inefficient to send them one by one, sequentially, waiting for each one to complete. The solution is to work asynchronously, meaning to send a request, not wait for it (send it in the background), and use threads to divide the number of requests and send multiple at once (concurrently). This would be the case when we don't know in advance the size of the problem we are solving, and either we need all the solutions to the problem (because we have to send all the requests) or we don't want to find all the solutions but at least one.
Below is an example program that uses ExecutorService to display all files in a directory (including each subdirectory). Because we don't know in advance how many files and directories we need to analyze, we can say that we don't know the size of the problem in this case.
import java.io.File;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
public class Example1 {
public static void main(String[] args) {
AtomicInteger inQueue = new AtomicInteger(0);
ExecutorService tpe = Executors.newFixedThreadPool(4);
inQueue.incrementAndGet();
tpe.submit(new MyRunnable("files", tpe, inQueue));
}
}
class MyRunnable implements Runnable {
String path;
ExecutorService tpe;
AtomicInteger inQueue;
public MyRunnable(String path, ExecutorService tpe, AtomicInteger inQueue) {
this.path = path;
this.tpe = tpe;
this.inQueue = inQueue;
}
@Override
public void run() {
File file = new File(path);
if (file.isFile()) {
System.out.println(file.getPath());
} else if (file.isDirectory()) {
File[] files = file.listFiles();
if (files != null) {
for (File f : files) {
inQueue.incrementAndGet();
tpe.submit(new MyRunnable(f.getPath(), tpe, inQueue));
}
}
}
int left = inQueue.decrementAndGet();
if (left == 0) {
tpe.shutdown();
}
}
}
As can be seen in the code above, the instantiation of an ExecutorService with 4 workers is done in the main thread. Through the submit() method, a new task is introduced into the pool, corresponding to the parent directory. The submit() method can receive an object that implements the Runnable interface and contains the run() method or one that implements the Callable interface and contains the call() method. The difference between them is that run() returns void, while call() returns an object and can also throw an exception. In other words, Callable is suitable when we are interested in the final result and any errors that may occur during task execution. In this lab, we will work with the Runnable interface.
In the example above, our class representing a task is named MyRunnable. In the run method, we check whether we are dealing with a file or a directory. If we are working on a file, we simply print its name. If we are working with a directory, it means that we need to create new tasks for each file or subdirectory contained within the parent directory. Thus, an instance of MyRunnable is created for each of them and added to the ExecutorService's pool.
In the case of the example presented in this lab, we need all solutions to the problem (all files and subdirectories), but we don't know the size of the problem in advance, so a way to stop is to keep track of the tasks in the pool and terminate execution when it becomes empty. Otherwise, our program would run indefinitely. The way to terminate the execution of an ExecutorService can be seen in the code above. Specifically, the shutdown() method is used to stop the ExecutorService from receiving new tasks. If we were interested in only one solution to our problem (not all, as in the example above), we could stop execution when we reached that solution.