Introducción al Paquete java.util.concurrent
El paquete java.util.concurrent
proporciona herramientas valiosas para la creación de aplicaciones concurrentes en Java. A medida que la necesidad de procesamiento eficiente y la respuesta rápida se vuelven esenciales en el desarrollo de software, el conocimiento de la concurrencia se ha vuelto impartible para los programadores. En este artículo, realizaremos una vista general de este paquete, resumiendo sus principales componentes y brindando ejemplos prácticos para que puedas implementarlos en tus proyectos y mejorar tus aplicaciones de Java.
Componentes Principales
El paquete java.util.concurrent
contiene muchas características demasiado extensas para discutir en una sola publicación. En este artículo, nos centraremos principalmente en algunas de las utilidades más útiles de este paquete, como:
- Executor
- ExecutorService
- ScheduledExecutorService
- Future
- CountDownLatch
- CyclicBarrier
- Semaphore
- ThreadFactory
- BlockingQueue
- DelayQueue
- Locks
- Phaser
También puedes encontrar varios artículos dedicados a clases individuales aquí. Ahora, profundicemos en cada uno de estos componentes.
Executor
Executor es una interfaz que representa un objeto que ejecuta las tareas proporcionadas. La implementación depende del contexto desde el cual se inicia la invocación, lo que significa que la tarea puede ejecutarse en un hilo nuevo o en el hilo actual. Usando esta interfaz, podemos desacoplar el flujo de ejecución de tareas del mecanismo real de ejecución de la tarea.
Un aspecto importante es que el Executor
no necesariamente requiere que la ejecución de la tarea sea asincrónica. En la implementación más simple, un ejecutor puede invocar la tarea enviada instantáneamente en el hilo que la invoca.
Ejemplo de código:
public class Invoker implements Executor {
@Override
public void execute(Runnable r) {
r.run();
}
}
public void execute() {
Executor executor = new Invoker();
executor.execute(() -> {
// tarea a realizar
});
}
Ten en cuenta que si el ejecutor no puede aceptar la tarea para su ejecución, lanzará una RejectedExecutionException
.
ExecutorService
ExecutorService es una solución completa para el procesamiento asíncrono. Maneja una cola en memoria y programa las tareas enviadas basándose en la disponibilidad de hilos. Para usar ExecutorService
, necesitamos crear una clase que implemente Runnable
.
Ejemplo de código:
public class Task implements Runnable {
@Override
public void run() {
// detalles de la tarea
}
}
ExecutorService executor = Executors.newFixedThreadPool(10);
public void execute() {
executor.submit(new Task());
}
También podemos crear una instancia de Runnable
al enviar la tarea.
executor.submit(() -> {
new Task();
});
Este servicio también tiene métodos de terminación de ejecución incorporados, como shutdown()
y shutdownNow()
que permiten cerrar el servicio correctamente.
ScheduledExecutorService
ScheduledExecutorService es una interfaz similar a ExecutorService
, pero tiene la capacidad de ejecutar tareas periódicamente y con retrasos.
Ejemplo de código:
ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
Future<String> future = executorService.schedule(() -> {
// ...
return "Hola mundo";
}, 1, TimeUnit.SECONDS);
executorService.scheduleAtFixedRate(() -> {
// ...
}, 1, 10, TimeUnit.SECONDS);
Future
Future se usa para representar el resultado de una operación asíncrona. Proporciona métodos para verificar si la operación asíncrona ha terminado, obtener el resultado calculado, etc.
Ejemplo de código:
Future<String> future = executorService.submit(() -> {
Thread.sleep(10000L);
return "Hola mundo";
});
if (future.isDone() && !future.isCancelled()) {
try {
String str = future.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
CountDownLatch
CountDownLatch es una clase utilitaria que bloquea un conjunto de hilos hasta que se completa una cierta operación.
Ejemplo de código:
CountDownLatch latch = new CountDownLatch(5); // Esperar 5 hilos
Runnable task = () -> {
// ejecutando tarea
latch.countDown(); // Decrementa el contador
};
for (int i = 0; i < 5; i++) {
new Thread(task).start();
}
latch.await(); // Espera hasta que el contador llegue a cero
CyclicBarrier
CyclicBarrier se utiliza para coordinar múltiples hilos que deben esperar a que todos lleguen a un punto específico.
Ejemplo de código:
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("Todos los hilos han llegado a la barrera.");
});
Runnable task = () -> {
try {
System.out.println(Thread.currentThread().getName() + " esperando en la barrera.");
barrier.await();
} catch (Exception e) {
e.printStackTrace();
}
};
new Thread(task).start();
new Thread(task).start();
new Thread(task).start();
Semaphore
Semaphore se utiliza para controlar el acceso de múltiples hilos a una sección crítica.
Ejemplo de código:
Semaphore semaphore = new Semaphore(3);
Runnable task = () -> {
try {
semaphore.acquire();
// sección crítica
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
}
};
for (int i = 0; i < 10; i++) {
new Thread(task).start();
}
ThreadFactory
ThreadFactory actúa como un generador de hilos (no existentes) que crea nuevos hilos bajo demanda.
Ejemplo de código:
public class CustomThreadFactory implements ThreadFactory {
private int threadId;
private String name;
public CustomThreadFactory(String name) {
threadId = 1;
this.name = name;
}
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r, name + "-Thread_" + threadId);
threadId++;
return t;
}
}
BlockingQueue
La interfaz BlockingQueue
es útil en patrones de integración asincrónicos como el patrón productor-consumidor. Permite que un hilo espere hasta que haya algún elemento que procesar.
Más información sobre BlockingQueue
está disponible aquí.
DelayQueue
El DelayQueue es una cola de bloqueo de tamaño infinito donde un elemento solo puede ser retirado si se cumple su tiempo de expiración.
Locks
Locks
es una utilidad para bloquear otros hilos de acceder a un segmento de código, a diferencia del bloque synchronized
, que está contenido dentro de un método.
Phaser
Phaser proporciona una solución más flexible que CyclicBarrier
y CountDownLatch
, permitiendo que un número dinámico de hilos esperen antes de continuar la ejecución, coordinando múltiples fases.
Más información sobre Phaser
está disponible aquí.
Conclusión
En este artículo de alto nivel, nos hemos centrado en las diferentes utilidades disponibles en el paquete java.util.concurrent
. La comprensión y el uso de estas herramientas pueden mejorar enormemente la eficiencia y la capacidad de respuesta de tus aplicaciones Java, ayudándote a crear soluciones más robustas y escalables. Al implementar estos conceptos, asegúrate de probar tus aplicaciones y evaluar su rendimiento en situaciones de carga concurrente. ¡Empieza a experimentar con estos componentes y lleva tu programación en Java al siguiente nivel!