¿Cómo implementar un java.util.concurrent.BlockingQueue en tus proyectos Java?

| Última modificación: 15 de enero de 2025 | Tiempo de Lectura: 4 minutos

Algunos de nuestros reconocimientos:

Premios KeepCoding

Las aplicaciones multihilo en Java han hecho llorar a más de un desarrollador principiante. Lo que pasa es que casi siempre presentan problema al sincronizar la comunicación entre diferentes hilos y muchos terminan pensando que no hay solución o que no existe una forma fácil de manejarlo. Por eso quiero que conozcas java.util.concurrent.BlockingQueue, un componente con el que podrás implementar patrones como el productor-consumidor sin esos riesgos de condiciones de carrera y otros errores que aparecen mucho en la programación concurrente.

java.util.concurrent.BlockingQueue

¿Qué es java.util.concurrent.BlockingQueue?

¿Has preparado galletas en el horno? Pues si es así, te has dado cuenta de que uno suele sacar las galletas para ponerlas en una bandeja mientras se terminan de enfriar para ser servidas. Entonces, java.util.concurrent.BlockingQueue sería como esa bandeja, donde tú almacenas temporalmente las tareas o elementos de datos hasta que alguien las necesite.

Lo que hace BlockingQueue es bloquear las operaciones de inserción o extracción dependiendo de su estado. Por eso es que, cuando intentas insertar un elemento en una cola llena o extraer uno de una cola vacía, el hilo se bloqueará automáticamente hasta que haya espacio o datos disponibles, respectivamente.

Funcionamiento

  • Bloqueo: Si la caja está llena y alguien intenta meter más galletas, se queda esperando hasta que haya espacio. Del mismo modo, si la caja está vacía y alguien quiere sacar una galleta, se queda esperando hasta que haya una disponible. Esto evita que se produzcan errores por intentar acceder a elementos que no existen o por intentar almacenar elementos en un lugar lleno.
  • Hilos múltiples: Imagina que tienes varios trabajadores (hilos) que se encargan de hornear las galletas y otros que se encargan de empacarlas. Todos ellos pueden acceder a la caja de manera segura, sin que se produzcan conflictos.
  • Orden: Las galletas se sacan de la caja en el mismo orden en que se metieron.

Tipos de BlockingQueue

  1. Cola no acotada (Unbounded): Puede crecer casi indefinidamente. Su capacidad está limitada por Integer.MAX_VALUE.
  2. Cola acotada (Bounded): Tiene una capacidad máxima definida. Cuando está llena, las operaciones de inserción se bloquean hasta que haya espacio disponible.

¿Cómo usar java.util.concurrent.BlockingQueue?

Ejemplo de inicialización

// Cola no acotada
BlockingQueue<String> unboundedQueue = new LinkedBlockingDeque<>();

// Cola acotada con capacidad de 10 elementos
BlockingQueue<String> boundedQueue = new LinkedBlockingDeque<>(10);

Métodos importantes de BlockingQueue

El interfaz BlockingQueue incluye métodos para agregar y recuperar elementos, con diferentes comportamientos dependiendo del estado de la cola:

Métodos de inserción

  • add(E e): Lanza una excepción si no hay espacio disponible.
  • put(E e): Bloquea hasta que haya espacio para insertar.
  • offer(E e): Devuelve true si la inserción fue exitosa, de lo contrario false.
  • offer(E e, long timeout, TimeUnit unit): Intenta insertar durante un período de tiempo especificado.

Métodos de extracción

  • take(): Bloquea hasta que haya un elemento disponible.
  • poll(long timeout, TimeUnit unit): Intenta recuperar un elemento, esperando un tiempo determinado antes de devolver null.

Implementación del patrón productor-consumidor con BlockingQueue

Uno de los usos más comunes de java.util.concurrent.BlockingQueue es el patrón productor-consumidor, donde un hilo produce datos y otro los consume.

Ejemplo: Productores y consumidores simples

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class ProducerConsumerExample {
public static void main(String[] args) {
BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(5);

// Productor
Runnable producer = () -> {
try {
for (int i = 1; i <= 10; i++) {
queue.put(i);
System.out.println("Producido: " + i);
Thread.sleep(500);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
};

// Consumidor
Runnable consumer = () -> {
try {
while (true) {
Integer item = queue.take();
System.out.println("Consumido: " + item);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
};

new Thread(producer).start();
new Thread(consumer).start();
}
}

Salida esperada

Producido: 1
Consumido: 1
Producido: 2
Producido: 3
Consumido: 2
...

Aquí usé una cola acotada con capacidad de 5 elementos. Si el productor genera datos más rápido de lo que el consumidor puede procesarlos, el productor se bloqueará hasta que haya espacio en la cola.

Manejando señales de finalización: Poison Pills

Cuando necesitas indicar a los consumidores que detengan su ejecución, puedes usar un “poison pill”, un elemento especial que actúa como una señal para terminar.

Ejemplo: Uso de “poison pills”

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class PoisonPillExample {
private static final int POISON_PILL = -1;

public static void main(String[] args) {
BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(5);

// Productor
Runnable producer = () -> {
try {
for (int i = 1; i <= 10; i++) {
queue.put(i);
System.out.println("Producido: " + i);
}
queue.put(POISON_PILL); // Señal de finalización
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
};

// Consumidor
Runnable consumer = () -> {
try {
while (true) {
Integer item = queue.take();
if (item.equals(POISON_PILL)) {
System.out.println("Consumidor finalizado");
break;
}
System.out.println("Consumido: " + item);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
};

new Thread(producer).start();
new Thread(consumer).start();
}
}

En este ejemplo, el productor inserta la “poison pill” al final de sus tareas. El consumidor la detecta y termina su ejecución de forma segura.

Algunas advertencias al usar java.util.concurrent.BlockingQueue

  • Elige el tipo de cola según las necesidades de tu aplicación (acotada o no acotada).
  • Siempre maneja interrupciones en las operaciones de cola para evitar bloqueos no deseados.

java.util.concurrent.BlockingQueue no te puede faltar para gestionar la comunicación entre hilos de manera eficiente y segura en Java. Ahora podrás manejar tareas de forma organizada y escalable.

Es hora de desbloquear tu potencial con disciplina y acompañado de KeepCoding. En el Bootcamp de Java Full Stack de KeepCoding aprenderás a dominar técnicas avanzadas como esta y mucho más. Transforma tu carrera y conviértete en un experto en desarrollo de software. ¡Inscríbete hoy mismo y comienza a crear el futuro!

Posts más leídos