La basta mayoría del software que utilizamos hoy en día trabaja con arquitectura multihilo, desde los servidores web que se inventan un thread cada vez que realizamos una solicitud HTTP, hasta las aplicaciones de escritorio cuando procesan un gran lote de datos y de esta forma la UI no se congela.

Prácticamente todos los frameworks vienen con un diseño multihilo implementado por debajo, de tal forma que nosotros solo debemos preocuparnos de programar la lógica sin interactuar con la parte más baja de la arquitectura.

En este artículo veremos un ejemplo simple de como crear una aplicación concurrente en Java, de esta forma podemos optimizar los tiempos de ejecución de nuestras aplicaciones y aprovechar al máximo los recursos de la máquina donde correrá.

Concurrent utils

El package java.util.concurrent contiene un buen puñado de clases e interfaces para trabajar con concurrencia. Algunas de las que vamos a utilizar para desarrollar el ejemplo son:

  • ExecutorService
  • Executor
  • Runnable

Ejemplo

Para nuestro ejemplo consultaremos la PokeAPI. Es una API pública que nos permite recuperar datos de cualquier pokemon.

En cada consulta a la API podemos obtener la info de un pokemon. En nuestro ejemplo consultaremos 3 pokemones, por cada uno haremos un llamado a la API.

Para evitar demoras, crearemos un hilo por cada llamado, de esta forma hacemos los 3 llamados al mismo tiempo de forma paralela y así nos ahorramos los tiempos de espera que habrían si hiciéramos las llamadas de forma secuencial.

Al hacer la invocación paralela, el proceso finaliza antes.

Creando la clase Runnable

Lo primero que vamos a hacer es la clase Runnable. La interfaz Runnable nos permite definir una clase que puede ser ejecutada de forma paralela a nuestro programa principal. A este tipo de clases le llamaremos Worker:

public PokeWorker implements Runnable {
    
    String pokemon;
    
    public PokeWorker(String pokemon) {
        this.pokemon = pokemon;
    }
    
    public void run() {
    
    }
    
}

Al implementar la interfaz Runnable debes crear el método Run, este será el método de entrada del hilo en cada instancia de PokeWorker.

Ahora definimos la lógica de la clase:

public void run() {
    System.out.println("Inicializando hilo...");

    // Hacemos el request a la API
    Request request = new Request
        .Builder()
        .url("https://pokeapi.co/api/v2/pokemon/" + this.pokemon)
        .build();

    String pokeResponse = new String();
    try (Response response = new OkHttpClient().newCall(request).execute()) {
        pokeResponse = response.body().string();
    } catch (IOException e) {
        System.out.println("API Error: " + e.getMessage());
    }

    // Parseamos la respuesta de la API
    Gson gson = new Gson();
    LinkedTreeMap parsed = gson.fromJson(pokeResponse, new LinkedTreeMap().getClass());

    // Obtenemos que tipo de pokemon es
    String parsedTypes = new String();
    for (Object type : (List) parsed.get("types")) {
        Object typeObject = ((LinkedTreeMap) type).get("type");
        parsedTypes += ((LinkedTreeMap) typeObject).get("name") + " ";
    }

    // Mostramos el #, nombre y tipos
    System.out.println(
        "#" + ((Double) parsed.get("order")).intValue() + 
        " - " + parsed.get("name") + 
        " - tipo: " + parsedTypes
    );
}

Se ve un poco tricky pero en realidad todo se resume a 4 pasos:

  1. Request a la API con la librería OkHttp
  2. Parseo del response JSON con la librería Gson
  3. Extraer data del response
  4. Mostrar la info en pantalla

Ahora que ya tenemos listo el Worker, hagamos algo de magia multihilo…

Creando la clase principal

En la clase principal:

public class App {

    public static void main(String args[]) throws InterruptedException {
    }
    
}

Definimos nuestro método main con un throws a InterruptedException, ya veremos porqué.

Luego creamos la lógica para nuestro programa:

public static void main(String args[]) throws InterruptedException {
   // Definimos los pokemones que vamos a buscar
    String[] pokemons = {"articuno", "zapdos", "moltres"};

    // Crea el thread pool indicando la cantidad de hilos que tendrá
    ExecutorService taskExecutor = Executors.newFixedThreadPool(pokemons.length);

    // Por cada pokemon, creamos un Worker y lo mandamos a ejecutar
    for (int i = 0; i < pokemons.length; i++) {
        PokeWorker pokeWorker = new PokeWorker(pokemons[i]);
        taskExecutor.execute(pokeWorker);
    }

    // Finalizamos el ingreso de nuevos hilos y esperamos a que termine cada uno
    taskExecutor.shutdown();
    taskExecutor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
}

Al igual que con el Worker, nuestro método principal ejecuta 4 simples pasos:

  1. Definimos cuales pokemones vamos a recuperar
  2. Creamos el thread pool diciéndole que debe tener 3 hilos
  3. Por cada pokemon creamos un Worker y lo ejecutamos inmediatamente
  4. Quedamos a la espera de que finalicen todos los hilos

Pero que pasa si un hilo se cae? Deberemos capturar esa excepción con un InterruptedException, razón por la cual la hemos agregado en la declaración del método main.

Analizando el output del programa

Cuando ejecutamos el programa y vemos la salida:

Inicializando hilo...
Inicializando hilo...
Inicializando hilo...
#218 - articuno - tipo: flying ice 
#220 - moltres - tipo: flying fire 
#219 - zapdos - tipo: flying electric 

A parte de decirnos que nuestros tres legendarios favoritos son de tipo volador, nos dice que los tres hilos se inician al mismo tiempo, y luego finalizan los tres de corrido. Es decir, no se ejecutaron de forma secuencial, si no en paralelo.

Si lo hubiésemos hecho de forma secuencial, nos hubiera resultado algo así:

Inicializando hilo...
#218 - articuno - tipo: flying ice 
Inicializando hilo...
#220 - moltres - tipo: flying fire 
Inicializando hilo...
#219 - zapdos - tipo: flying electric 

El problema con esto es que para hacer un llamado a la API debemos esperar a que el llamado anterior termine. Al hacerlo multihilo tiramos todos los llamados de un paraguazo, así nos ahorramos el tiempo de espera en que finaliza un llamado para comenzar el otro.

Final notes

El mundo de la concurrencia en Java es bastante extenso, mencionar que al día de hoy el lenguaje nos da un excelente soporte para este tipo de aplicaciones, permitiéndonos aprovechar estas capacidades con facilidad. Si te interesa profundizar más en esta área te recomiendo leer el tutorial de Jenkov.

Los fuentes del ejemplo puedes encontrarlos en este repo.