3 - Runnable, Callable y Future

Java separa la definición de una tarea del mecanismo para ejecutarla. Con Runnable describes trabajo sin retorno, con Callable devuelves valores y con Future recuperas el resultado o cancelas la ejecución.

3.1 Runnable

  • Tarea sin valor de retorno: run() es void; ideal para tareas que solo producen efectos secundarios.
  • Buen uso: labores fire-and-forget como registrar métricas o limpiar archivos temporales.
  • Límites: no permite devolver valores ni declarar excepciones checked; debes capturarlas dentro de la tarea.

3.2 Callable

  • Tarea con retorno: call() devuelve un valor genérico T.
  • Permite excepciones: puede lanzar checked exceptions, propagadas luego como ExecutionException al leer el resultado.
  • Ideal para cómputos complejos: consultas a BD, cálculos intensivos o transformaciones que deben devolver datos.
import java.util.concurrent.*;

public class DemoCallableFuture {
  public static void main(String[] args) throws Exception {
    ExecutorService pool = Executors.newSingleThreadExecutor();

    Callable<Integer> tarea = () -> {
      TimeUnit.MILLISECONDS.sleep(200);
      return 42;
    };

    Future<Integer> futuro = pool.submit(tarea);

    if (!futuro.isDone()) {
      System.out.println("Sigue en curso...");
    }

    try {
      Integer resultado = futuro.get(); // bloquea hasta terminar
      System.out.println("Resultado: " + resultado);
    } catch (ExecutionException e) {
      System.err.println("Fallo interno: " + e.getCause());
    } finally {
      pool.shutdown();
    }
  }
}

3.3 Future

  • Recuperar el resultado: se obtiene al enviar un Callable o Runnable a un ExecutorService.
  • Métodos clave: get() (bloquea hasta tener el valor), isDone() (saber si terminó), cancel() (intenta detenerlo, opcionalmente interrumpiendo).
  • Manejo de excepciones: get() envuelve errores en ExecutionException; revisa getCause() para el origen.
  • Bloqueo vs no bloqueo: get(timeout) evita bloqueos indefinidos; combinaciones como isDone() + get(0, TimeUnit.SECONDS) ayudan a consultar sin esperar.

Ejemplo que dispara una excepción por timeout al leer el resultado de un Future; ilustra cómo reaccionar cuando la tarea tarda más de lo esperado.

import java.util.concurrent.*;

public class DemoCallableFuture {
  public static void main(String[] args) throws Exception {
    ExecutorService pool = Executors.newSingleThreadExecutor();

    Callable<Integer> tarea = () -> {
      TimeUnit.MILLISECONDS.sleep(2_000);
      return 42;
    };

    Future<Integer> futuro = pool.submit(tarea);

    if (!futuro.isDone()) {
      System.out.println("Sigue en curso...");
    }

    try {
      Integer resultado = futuro.get(1, TimeUnit.SECONDS); // lanza TimeoutException
      System.out.println("Resultado: " + resultado);
    } catch (TimeoutException e) {
      System.err.println("Tardó demasiado: " + e);
      futuro.cancel(true); // intenta interrumpir
    } catch (ExecutionException e) {
      System.err.println("Fallo interno: " + e.getCause());
    } finally {
      pool.shutdown();
    }
  }
}

La excepción surge porque get(1, TimeUnit.SECONDS) espera solo un segundo, mientras la tarea duerme dos segundos. Al excederse el límite se lanza TimeoutException y el código intenta cancelar el futuro con interrupción cooperativa.

Este patrón resulta útil cuando consultas un microservicio o base externa con SLO estricto: si la respuesta no llega en 1s, cancelas la tarea, devuelves un fallback o pasas al siguiente nodo sin bloquear el hilo consumidor.

3.4 Problemas típicos con Future

  • get() bloqueante: si la tarea se queda colgada, el hilo consumidor también; usa timeouts.
  • Sin composición: no permite encadenar tareas ni combinar resultados de forma declarativa.
  • Cancelaciones complicadas: depende de que la tarea coopere con interrupciones.
  • Alternativas modernas: CompletableFuture (mencionado sin profundizar) ofrece encadenamiento, manejo asíncrono y mejor composición.