En el desarrollo concurrente con Java, se recomienda evitar el uso de la clase Executors para crear pools de hilos, ya que puede ocultar riesgos de agotamiento de recursos. En su lugar, se debe emplear ThreadPoolExecutor, lo que permite un control explícito sobre las reglas de ejecución del pool.
La clase java.util.concurrent.ThreadPoolExecutor proporciona un constructor común con los siguientes parámetros:
ThreadPoolExecutor(int nucleosHilos, int maxHilos,
long tiempoInactividad, TimeUnit unidad,
BlockingQueue colaTrabajos,
RejectedExecutionHandler manejador)
Donde:
- nucleosHilos: Número mínimo de hilos manteniéndose activos en el pool.
- maxHilos: Número máximo de hilos permitidos en el pool.
- tiempoInactividad: Tiempo que un hilo puede permanecer inactivo antes de ser finalizado.
- unidad: Unidad de tiempo para el parámetro anterior, como
TimeUnit.SECONDS. - colaTrabajos: Cola de bloqueo para almacenar tareas pendientes, por ejemplo,
ArrayBlockingQueue. - manejador: Estrategia para rechazar tareas cuando el pool está saturado, como
AbortPolicyoCallerRunsPolicy.
Una tarea se añade al pool mediante el método execute(Runnable), donde el objeto Runnable define la lógica de ejecución. Al agregar una tarea, se sigue esta prioridad:
- Si el número de hilos activos es menor que nucleosHilos, se crea un nuevo hilo.
- Si el número de hilos alcanza nucleosHilos pero la cola no está llena, la tarea se encola.
- Si la cola está llena y los hilos activos son menores que maxHilos, se crea un hilo adicional.
- Si tanto la cola como el límite de hilos están saturados, el manejador decide cómo proceder.
Cuando los hilos inacitvos exceden el tiempoInactividad, se finalizan dinámicamente para optimizar recursos.
Las opciones para mnaejador incluyen:
AbortPolicy: Lanza una excepciónRejectedExecutionException.CallerRunsPolicy: Reintenta la ejecución en el hilo llamante.DiscardOldestPolicy: Descarta la tarea más antigua en la cola.DiscardPolicy: Descarta silenciosamente la tarea rechazada.
Ejemplo básico de uso con un pool configurado para pruebas:
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class DemoPoolHilos {
private static final int TOTAL_TAREAS = 10;
private static final int PAUSA_ENVIO = 200; // milisegundos
public static void main(String[] args) {
// Crear pool con 2 hilos mínimos, 4 máximo, inactividad de 3 seg
ThreadPoolExecutor pool = new ThreadPoolExecutor(2, 4, 3, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(3), new ThreadPoolExecutor.DiscardOldestPolicy());
for (int idx = 0; idx < TOTAL_TAREAS; idx++) {
String idTarea = "Tarea-" + (idx + 1);
pool.execute(new TareaRunnable(idTarea));
try {
Thread.sleep(PAUSA_ENVIO);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
pool.shutdown();
}
}
class TareaRunnable implements Runnable {
private final String identificador;
private static final long DURACION_SIMULADA = 1500; // milisegundos
public TareaRunnable(String id) {
this.identificador = id;
}
@Override
public void run() {
System.out.println("Procesando: " + identificador + " en " + Thread.currentThread().getName());
try {
Thread.sleep(DURACION_SIMULADA);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
En este ejemplo, el pool gestiona tareas con un buffer limitado. Si la cola se llena, se aplica la política de descarte más antiguo. Para experimentar, se pueden ajustar los tiempos de pausa y la estrategia de rechazo.
Otro patrón común implica controlar la adición de tareas según el estado de la cola:
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class PoolControlado {
private static final int PROFUNDIDAD_COLA = 4;
public void ejecutar() {
ThreadPoolExecutor ejecutor = new ThreadPoolExecutor(2, 4, 3, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(PROFUNDIDAD_COLA), new ThreadPoolExecutor.DiscardOldestPolicy());
for (int iter = 0; iter < 12; iter++) {
while (ejecutor.getQueue().size() >= PROFUNDIDAD_COLA) {
System.out.println("Esperando espacio en la cola...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
final int numTarea = iter;
ejecutor.execute(() -> {
System.out.println("Ejecutando tarea " + numTarea);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
ejecutor.shutdown();
}
public static void main(String[] args) {
new PoolControlado().ejecutar();
}
}
Aquí, se verifica activamente el tamaño de la cola antes de añadir tareas, evitando rechazos inesperados. Este enfoque es útil cuando se requiere una retroalimentación más fina sobre la carga del sistema.