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. 🚦
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:
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.
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. 🔄
A lo largo de mi experiencia, he usado semáforos en los siguientes escenarios:
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í |
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. ⚖️
Después de algunos errores que me costaron horas de depuración, aprendí estas lecciones clave al trabajar con semáforos:
finally
, para evitar fugas en caso de excepciones.
✅ Usados correctamente, los semáforos te dan un control poderoso y eficiente.
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. 🧩
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. ⚙️
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! 🚀
Jorge García
Fullstack developer