Multitasking and Threads in Java with Examples II (Runnable & Executors)
Learn how to manage a queue of tasks in Java with Runnable, ExecutorService and Executors, using a supermarket checkout example with 8 customers and 2 cashiers.
Series: Multithreading in Java part 2 of 2
You can download the project for this post by clicking HERE.
NOTE: Throughout this post, the word “thread” (lowercase t) refers to the concept of an independent thread of execution, and the word “Thread” (uppercase T) refers to Java’s Thread class.
In the post “Multitasking and Threads in Java with Examples (Thread & Runnable)” we saw the different ways of working with Threads in Java, either by inheriting from the Thread class or by implementing the Runnable interface. We also saw the advantages of multitasking and how our programs can run processes in parallel as long as they are independent from each other.
As an example in the previous post, we simulated a checkout process in a supermarket where two customers come with a cart full of products, and one cashier (one thread) or two cashiers (two threads) pass the products over the scanner to charge them for their purchase. The customers’ products were represented by an array of integers (int) where each integer represented the seconds the cashier took to process that product. The example had a small catch: we used as many cashiers (or threads) as there were customers, so we put two cashiers to process two customers’ products, but what would happen if instead of two customers (2 processes) we had 100 customers? Would we have to put 100 cashiers (create 100 threads)?. Another thing we explained is that threads are parallel executions and are closely tied to the number of cores (or processing units) of our hardware (or computer), so we must take hardware resources into account when implementing things with threads.
In short, what we want to show in this post is how to manage the execution of threads. Imagine the supermarket example again, but this time instead of 2 customers we have 8 customers (8 processes) and two cashiers (2 threads). Obviously, the customers have to be served one at a time, so they must be put in a queue and processed one by one. We could implement our own “queue or stack” system to manage threads, but Java already provides the “ExecutorService” interface and the “Executors” class (which gives us implementations of the ExecutorService interface) to manage threads. The “Executors” class is in charge of managing the execution of the threads based on the number of threads we use. To draw a parallel with our example, the “Executors” class will organize the queue of Customers and send each Customer to the corresponding Cashier once she has finished processing the previous Customer’s purchase. Let’s look at the following example:
We define the Cliente class with a “nombre” attribute and an array of integers, where each integer represents a product in the cart and its value is the time the cashier takes to scan that product. That is, if we have an array [1,3,5], it means the customer bought 3 products and the cashier will take ‘1 second’ to process product 1, ‘3 seconds’ for product 2 and ‘5 seconds’ for product 3, so it will take ‘9 seconds’ to charge the customer for the whole purchase:
public class Cliente {
private String nombre;
private int[] carroCompra;
// Constructor, getter and setter
}
To use the Executors class, we must implement the “Runnable” interface and not override (or inherit from) the “Thread” class, since the Executors class only manages classes that implement the Runnable interface; therefore, we implement the Cashier class as follows:
public class CajeraRunnable implements Runnable {
private Cliente cliente;
private long initialTime;
public CajeraRunnable(Cliente cliente, long initialTime) {
this.cliente = cliente;
this.initialTime = initialTime;
}
@Override
public void run() {
System.out.println("La cajera " + Thread.currentThread().getName()
+ " COMIENZA A PROCESAR LA COMPRA DEL CLIENTE " + this.cliente.getNombre()
+ " EN EL TIEMPO: " + (System.currentTimeMillis() - this.initialTime) / 1000 + "seg");
for (int i = 0; i < this.cliente.getCarroCompra().length; i++) {
// The order is processed in X seconds
this.esperarXsegundos(cliente.getCarroCompra()[i]);
System.out.println("Procesado el producto " + (i + 1) + " del " + this.cliente.getNombre()+
"->Tiempo: " + (System.currentTimeMillis() - this.initialTime) / 1000 + "seg");
}
System.out.println("La cajera " + Thread.currentThread().getName() + " HA TERMINADO DE PROCESAR "
+ this.cliente.getNombre() + " EN EL TIEMPO: "
+ (System.currentTimeMillis() - this.initialTime) / 1000 + "seg");
}
private void esperarXsegundos(int segundos) {
try {
Thread.sleep(segundos * 1000);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
// getter and setter
Finally, here is the code that runs the purchase process:
public class MainExecutor {
private static final int numCajeras = 2;
public static void main(String[] args) {
ArrayList<Cliente>clientes = new ArrayList<Cliente>();
clientes.add(new Cliente("Cliente 1", new int[] { 2, 2, 1, 5, 2 })); // 12 Sec
clientes.add(new Cliente("Cliente 2", new int[] { 1, 1, 5, 1, 1 })); // 9 Sec
clientes.add(new Cliente("Cliente 3", new int[] { 5, 3, 1, 5, 2 })); // 16 Sec
clientes.add(new Cliente("Cliente 4", new int[] { 2, 4, 3, 2, 5 })); // 16 Sec
clientes.add(new Cliente("Cliente 5", new int[] { 1, 3, 2, 2, 3 })); // 11 Sec
clientes.add(new Cliente("Cliente 6", new int[] { 4, 2, 1, 3, 1 })); // 11 Sec
clientes.add(new Cliente("Cliente 7", new int[] { 3, 3, 2, 4, 7 })); // 19 Sec
clientes.add(new Cliente("Cliente 8", new int[] { 6, 1, 3, 1, 3 })); // 14 Sec
// Total time to process all the orders = 108 seconds
long init = System.currentTimeMillis(); // Initial instant of the processing
ExecutorService executor = Executors.newFixedThreadPool(numCajeras);
for (Cliente cliente: clientes) {
Runnable cajera = new CajeraRunnable(cliente, init);
executor.execute(cajera);
}
executor.shutdown(); // I shut down the Executor
while (!executor.isTerminated()) {
// I wait for all the processes to finish running
// before moving on to the next instructions
}
long fin = System.currentTimeMillis(); // Final instant of the processing
System.out.println("Tiempo total de procesamiento: "+(fin-init)/1000+" Segundos");
}
}
This code snippet is where the whole execution of the threads is managed. First, we created 8 Cliente objects and put them in an ArrayList called clientes. Then we wrote the following:
ExecutorService executor = Executors.newFixedThreadPool(numCajeras);
We created an object from the Executors class, and with the “newFixedThreadPool()” method we are telling it to fix the number of threads to run at ‘2’ (since numCajeras = 2); that is, it will handle the management of the processes to run in the program’s threads. The “ThreadPool” part is there because this Executors framework is based on the concept of managing a pool of threads, which is why the threads it creates are given names like “pool-1-thread-1“, “pool-1-thread-2“, etc.
Once the “executor” is declared, we launch the processes (that is, the customers get put in the queue) to be processed. To do so, we loop through the ArrayList and say that each Customer’s order will be processed by a cashier (Runnable cajera = new CajeraRunnable(cliente, init)), and we also say that the Executor is the one in charge of managing the thread queue (executor.execute(cajera)):
for (Cliente cliente: clientes) {
Runnable cajera = new CajeraRunnable(cliente, init);
executor.execute(cajera);
}
What we are telling the “executor” object this way is that it has to process the 8 Customers in the ArrayList, and that it will take care of processing them in order, using two threads; in other words, it manages the process queue. As a result of running the program, we get the following output (with 2 threads):
"La cajera pool-1-thread-1" COMIENZA A PROCESAR LA COMPRA DEL CLIENTE Cliente 1 EN EL TIEMPO: 0seg
"La cajera pool-1-thread-2" COMIENZA A PROCESAR LA COMPRA DEL CLIENTE Cliente 2 EN EL TIEMPO: 0seg
Procesado el producto 1 del Cliente 2->Tiempo: 1seg
Procesado el producto 1 del Cliente 1->Tiempo: 2seg
...............
Procesado el producto 4 del Cliente 2->Tiempo: 8seg
Procesado el producto 5 del Cliente 2->Tiempo: 9seg
"La cajera pool-1-thread-2" HA TERMINADO DE PROCESAR Cliente 2 EN EL TIEMPO: 9seg
"La cajera pool-1-thread-2" COMIENZA A PROCESAR LA COMPRA DEL CLIENTE Cliente 3 EN EL TIEMPO: 9seg
Procesado el producto 4 del Cliente 1->Tiempo: 10seg
Procesado el producto 5 del Cliente 1->Tiempo: 12seg
"La cajera pool-1-thread-1" HA TERMINADO DE PROCESAR Cliente 1 EN EL TIEMPO: 12seg
"La cajera pool-1-thread-1" COMIENZA A PROCESAR LA COMPRA DEL CLIENTE Cliente 4 EN EL TIEMPO: 12seg
Procesado el producto 1 del Cliente 3->Tiempo: 14seg
Procesado el producto 1 del Cliente 4->Tiempo: 14seg
...............
Procesado el producto 4 del Cliente 4->Tiempo: 23seg
Procesado el producto 5 del Cliente 3->Tiempo: 25seg
"La cajera pool-1-thread-2" HA TERMINADO DE PROCESAR Cliente 3 EN EL TIEMPO: 25seg
"La cajera pool-1-thread-2" COMIENZA A PROCESAR LA COMPRA DEL CLIENTE Cliente 5 EN EL TIEMPO: 25seg
Procesado el producto 1 del Cliente 5->Tiempo: 26seg
Procesado el producto 5 del Cliente 4->Tiempo: 28seg
"La cajera pool-1-thread-1" HA TERMINADO DE PROCESAR Cliente 4 EN EL TIEMPO: 28seg
"La cajera pool-1-thread-1" COMIENZA A PROCESAR LA COMPRA DEL CLIENTE Cliente 6 EN EL TIEMPO: 28seg
Procesado el producto 2 del Cliente 5->Tiempo: 29seg
...............
Procesado el producto 5 del Cliente 5->Tiempo: 36seg
"La cajera pool-1-thread-2" HA TERMINADO DE PROCESAR Cliente 5 EN EL TIEMPO: 36seg
"La cajera pool-1-thread-2" COMIENZA A PROCESAR LA COMPRA DEL CLIENTE Cliente 7 EN EL TIEMPO: 36seg
Procesado el producto 4 del Cliente 6->Tiempo: 38seg
Procesado el producto 1 del Cliente 7->Tiempo: 39seg
Procesado el producto 5 del Cliente 6->Tiempo: 39seg
"La cajera pool-1-thread-1" HA TERMINADO DE PROCESAR Cliente 6 EN EL TIEMPO: 39seg
"La cajera pool-1-thread-1" COMIENZA A PROCESAR LA COMPRA DEL CLIENTE Cliente 8 EN EL TIEMPO: 39seg
Procesado el producto 2 del Cliente 7->Tiempo: 42seg
...............
Procesado el producto 5 del Cliente 8->Tiempo: 53seg
"La cajera pool-1-thread-1" HA TERMINADO DE PROCESAR Cliente 8 EN EL TIEMPO: 53seg
Procesado el producto 5 del Cliente 7->Tiempo: 55seg
"La cajera pool-1-thread-2" HA TERMINADO DE PROCESAR Cliente 7 EN EL TIEMPO: 55seg
Tiempo total de procesamiento: 55 Segundos
Another very important thing is to “shut down” the executor, because even though we only tell it to run 8 Customers, it will keep asking (polling) whether there are more processes to run; so you can check that if you remove “executor.shutdown()” from the code, the program will never finish running.
You also need to control the flow of the program: if you want the program not to run the following instructions until the processes run by the “executor” have finished, you have to write a “while” loop that constantly asks whether the executor has finished or not, like this:
while (!executor.isTerminated()) {
// I wait for all the processes to finish running
// before moving on to the next instructions
}
If we didn’t control this, the “executor” would run the assigned processes on its own while the program carried on with its execution.
In short, this is the simplest way to manage a queue of processes that must be run on a given number of threads. In this example, since there are 8 customers, we could have run all 8 Customers in parallel; that is, by setting “numcajeras = 8“, and the program’s execution time would have been 19 seconds, which is the time it would take to process the most costly customer. Nothing bad would have happened, since Java manages threads quite well, and even if our PC has 2 cores it can run 8 threads or more at the same time without us noticing. But think about this: if instead of 8 Customers we had 2 or 3 million customers, we couldn’t launch 2 or 3 million threads, because there would be a spectacular amount of overhead and the operating system would take much longer to assign CPU to the 2 or 3 million processes than to actually run them. Therefore, it is very important to know how to build a good process queue system so you can run as many processes in parallel as possible, and in Java the Executor Framework lets us do that very easily, as we have seen in this post.
“Multitasking and Threads in Java with Examples II (Runnable & Executors)” by Ricardo Moya at jarroba.com, licensed under Creative Commons Attribution-NonCommercial-ShareAlike 3.0. Registered at Safe Creative.

You may also like
Multitasking in Android
Learn how multitasking and threads work in Android: the main thread, background threads, event-driven programming and why you should never block the UI.
Multitasking and Threads: Easy, With Lots of Benefits
Learn what multitasking and threads are, how concurrency and parallelism differ, and why cores matter, explained with simple comic-style examples.

Leave a comment