Volver a la página principal
miércoles 23 abril 2025
3

Cómo usar Semáforos en Java: Sincronización efectiva de hilos

Cuando empecé a trabajar con aplicaciones multihilo en Java, uno de los mayores retos fue evitar condiciones de carrera y garantizar un acceso controlado a recursos compartidos. Entre las diversas herramientas que Java ofrece, los semáforos resultaron ser una de las más efectivas y flexibles. En este artículo te enseñaré cómo funcionan, cuándo usarlos y cómo implementarlos correctamente. 🚦

🧠 ¿Qué es un semáforo en programación?

Un semáforo es una construcción de sincronización que controla el acceso a uno o más recursos compartidos. A diferencia de un synchronized, que permite acceso exclusivo, un semáforo puede permitir múltiples accesos concurrentes hasta cierto límite.

En Java, los semáforos se implementan a través de la clase java.util.concurrent.Semaphore.

Existen dos tipos principales:

  • Semáforo binario: permite un solo permiso (similar a un candado).
  • Semáforo con conteo: permite un número determinado de permisos concurrentes.

🛠️ ¿Cómo se usa un semáforo en Java?

Aquí te muestro un ejemplo básico usando un semáforo con conteo para simular el acceso de múltiples hilos a un recurso limitado.

Ejemplo básico

import java.util.concurrent.Semaphore;

public class EjemploSemaforo {

    private static final Semaphore semaforo = new Semaphore(3); // permite 3 hilos a la vez

    public static void main(String[] args) {
        for (int i = 1; i <= 10; i++) {
            final int hiloId = i;
            new Thread(() -> accederRecurso(hiloId)).start();
        }
    }

    public static void accederRecurso(int id) {
        try {
            System.out.println("Hilo " + id + " intentando acceder...");
            semaforo.acquire(); // solicita un permiso
            System.out.println("Hilo " + id + " accediendo al recurso...");
            Thread.sleep(2000); // simulamos uso del recurso
            System.out.println("Hilo " + id + " liberando recurso.");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            semaforo.release(); // libera el permiso
        }
    }
}

Este ejemplo permite que solo 3 hilos accedan simultáneamente al recurso compartido, y el resto debe esperar a que se liberen permisos. 🔄

🤔 ¿Cuándo usar un semáforo en Java?

A lo largo de mi experiencia, he usado semáforos en los siguientes escenarios:

  • Controlar el acceso simultáneo a recursos limitados, como conexiones de base de datos.
  • Implementar barreras o límites de concurrencia.
  • Evitar sobrecarga de servicios externos.
  • Sincronizar flujos de trabajo que dependen de una cantidad determinada de señales antes de continuar.

🔍 Diferencias entre synchronized, Lock y Semaphore

Una de las dudas más comunes al iniciarse con concurrencia en Java es cuándo usar semáforos en lugar de otras técnicas como synchronized o Lock. Aquí va una comparación útil:

Característica synchronized Lock Semaphore
Tipo de acceso Exclusivo Exclusivo y configurable Compartido y configurable
Permite múltiples hilos No No Sí (configurable)
Control granular Bajo Alto Muy alto
Posibilidad de timeout No Sí (tryAcquire())

Si necesitas controlar cuántos hilos acceden a un recurso, usa un semáforo. Si solo quieres evitar acceso simultáneo, un synchronized puede ser suficiente. ⚖️

⚠️ Buenas prácticas al usar semáforos

Después de algunos errores que me costaron horas de depuración, aprendí estas lecciones clave al trabajar con semáforos:

  • Siempre libera el permiso en un bloque finally, para evitar fugas en caso de excepciones.
  • Evita deadlocks: asegúrate de no intentar adquirir más permisos de los necesarios.
  • No uses semáforos para lógica innecesaria: si solo necesitas exclusión mutua, no uses un semáforo de conteo.
  • Monitorea el rendimiento si estás implementando límites de concurrencia en entornos de alta carga.

✅ Usados correctamente, los semáforos te dan un control poderoso y eficiente.

📌 Otros métodos útiles de la clase Semaphore

Además de acquire() y release(), aquí tienes otros métodos que te pueden ser muy útiles:

semaphore.tryAcquire(); // intenta adquirir permiso sin bloquear
semaphore.tryAcquire(5, TimeUnit.SECONDS); // espera cierto tiempo
semaphore.availablePermits(); // verifica cuántos permisos están disponibles
semaphore.drainPermits(); // consume todos los permisos disponibles

Estos métodos te permiten tener un control más fino y diseñar sistemas más robustos. 🧩

🧪 Ejemplo avanzado: Límite de conexiones simultáneas

Imagina que estás manejando una API que solo permite 5 conexiones simultáneas. Aquí es donde un semáforo brilla:

public class APILimitador {
    private final Semaphore limiteConexiones = new Semaphore(5);

    public void manejarPeticion(int id) {
        if (limiteConexiones.tryAcquire()) {
            try {
                System.out.println("Petición " + id + " procesándose.");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                limiteConexiones.release();
                System.out.println("Petición " + id + " completada.");
            }
        } else {
            System.out.println("Petición " + id + " rechazada por exceso de carga.");
        }
    }
}

Con este patrón, puedes rechazar solicitudes de forma segura si el sistema está sobrecargado. ⚙️

🧾 Conclusión

Los semáforos en Java son una herramienta poderosa para gestionar concurrencia de forma flexible. Al aprender a usarlos correctamente, puedes evitar cuellos de botella, prevenir errores comunes y construir sistemas más estables y eficientes.

Personalmente, me ayudaron a entender mejor la naturaleza de los hilos y los recursos compartidos, y han sido clave en varias optimizaciones de rendimiento en proyectos reales.

Si te gustó este artículo, ¡no olvides compartirlo y seguirme para más contenidos sobre Java, concurrencia y buenas prácticas en desarrollo de software! 🚀

Etiquetas:
java
Compartir:
Creado por:
Author photo

Jorge García

Fullstack developer