1. Mutex: qué es y cómo funciona
Mutex (del inglés «mutual exclusion», «exclusión mutua») es un mecanismo que permite que solo un hilo ejecute a la vez una sección crítica de código. Si el mutex está ocupado (retenido por otro hilo), los demás hilos esperan hasta que se libere.
En Java, el papel de mutex lo cumple a menudo el objeto sobre el que se sincroniza el código: synchronized. A partir de la 5 versión de Java apareció la clase ReentrantLock, una implementación de mutex más explícita y flexible.
Esquemáticamente
Imagine una habitación con una única llave (el mutex). Para entrar, hay que coger la llave. Si no está (alguien ya la cogió), esperas en la puerta. En cuanto la llave vuelve a su sitio (se libera el mutex), la siguiente persona puede entrar.
Sintaxis del mutex en Java
Con synchronized (clásico):
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
}
Aquí todo el método increment está protegido por un mutex: solo un hilo puede ejecutarlo en un momento dado.
Con ReentrantLock (más flexible):
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private int count = 0;
private final Lock lock = new ReentrantLock();
public void increment() {
lock.lock(); // Capturamos el mutex
try {
count++;
} finally {
lock.unlock(); // ¡Liberar siempre!
}
}
}
¡Importante! Libera siempre el mutex en el bloque finally; de lo contrario, puedes provocar un «bloqueo eterno» (deadlock) y el programa se quedará colgado.
¿Cuándo se necesita un mutex?
Un mutex es necesario cuando a un recurso solo debe acceder un hilo cada vez. Puede ser una variable, un archivo o una base de datos. Es especialmente importante usar un mutex si el trabajo con el recurso no es atómico: incluso un simple count++ en realidad consta de tres pasos — leer el valor, incrementarlo y escribirlo de nuevo. Sin un mutex, varios hilos pueden intercalarse entre los pasos y provocar una condición de carrera.
2. Semáforo (Semaphore): para qué sirve y cómo funciona
Semáforo — «regulador» que permite que varios hilos trabajen simultáneamente con un recurso, pero no más de una cantidad establecida. Si se alcanza el límite, los demás hilos esperan su turno.
Analogía: un aparcamiento para 3 coches. Si todas las plazas están ocupadas, los recién llegados esperan hasta que alguien se vaya.
Sintaxis del semáforo en Java
Para ello se utiliza la clase Semaphore del paquete java.util.concurrent:
import java.util.concurrent.Semaphore;
public class ParkingLot {
private final Semaphore spots;
public ParkingLot(int places) {
this.spots = new Semaphore(places);
}
public void parkCar(String car) throws InterruptedException {
spots.acquire(); // Intentamos ocupar una plaza (si no hay — esperamos)
try {
System.out.println(car + " se ha aparcado.");
Thread.sleep(1000); // El coche está en el aparcamiento
} finally {
spots.release(); // Liberamos la plaza
System.out.println(car + " se ha ido.");
}
}
}
Uso:
ParkingLot parking = new ParkingLot(3);
for (int i = 1; i <= 5; i++) {
final String car = "Coche " + i;
new Thread(() -> {
try {
parking.parkCar(car);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
Resultado: nunca habrá más de tres coches a la vez en el aparcamiento; los demás esperan.
¿Cómo funciona un semáforo?
- Al crear un semáforo se define el número de «permisos» (permits).
- El método acquire() intenta tomar un permiso: si hay uno libre, el hilo pasa; si no, espera.
- El método release() devuelve el permiso.
- Un semáforo con un solo permiso se comporta casi como un mutex, pero sin «propietario».
3. Mutex y semáforo: ¿en qué se diferencian?
| Característica | Mutex | Semáforo (Semaphore) |
|---|---|---|
| Número de hilos | Solo uno | Varios (número limitado) |
| Aplicación | Protección del recurso | Limitación de acceso (por ejemplo, un pool) |
| API en Java | |
|
| Control | Normalmente el «propietario» | Puede liberar cualquier hilo |
| Escenario típico | Contador compartido, objeto | Pool de conexiones, aparcamiento, límite |
- Mutex — para casos donde se necesita acceso exclusivo.
- Semáforo — cuando se puede dejar pasar a varios, pero no a todos.
Analogía: un mutex es un baño con una sola cabina; un semáforo es un baño con tres cabinas.
4. Ejemplos prácticos
Ejemplo 1: Mutex para proteger la sección crítica
Supongamos que tenemos un banco compartido y varios hilos transfieren dinero entre cuentas. Las operaciones deben ser atómicas.
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class BankAccount {
private int balance;
private final Lock lock = new ReentrantLock();
public BankAccount(int initial) {
this.balance = initial;
}
public void deposit(int amount) {
lock.lock();
try {
balance += amount;
} finally {
lock.unlock();
}
}
public void withdraw(int amount) {
lock.lock();
try {
if (balance >= amount) {
balance -= amount;
}
} finally {
lock.unlock();
}
}
public int getBalance() {
return balance;
}
}
Aquí cualquier operación con el saldo está protegida por un mutex para evitar una race condition.
Ejemplo 2: Semáforo para limitar el acceso
El servidor solo puede atender simultáneamente a 2 clientes (por ejemplo, por la licencia).
import java.util.concurrent.Semaphore;
public class Server {
private final Semaphore connections = new Semaphore(2);
public void handleRequest(String client) throws InterruptedException {
connections.acquire();
try {
System.out.println(client + " se ha conectado al servidor.");
Thread.sleep(2000); // Simulación del procesamiento de la petición
} finally {
connections.release();
System.out.println(client + " se ha desconectado.");
}
}
}
Uso:
Server server = new Server();
for (int i = 1; i <= 5; i++) {
final String client = "Cliente " + i;
new Thread(() -> {
try {
server.handleRequest(client);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
Resultado: el servidor atiende como máximo a dos clientes a la vez.
5. Particularidades y matices de uso
Mutex: ¡libéralo siempre!
Es muy importante no olvidar llamar a unlock() (o salir del bloque sincronizado) incluso si hay excepciones. Usa try-finally:
lock.lock();
try {
// sección crítica
} finally {
lock.unlock();
}
Si lo olvidas, puedes provocar un «bloqueo eterno»; los demás hilos esperarán indefinidamente.
Semáforo: ¿se puede liberar desde otro hilo?
A diferencia del mutex, release() puede llamarlo cualquier hilo, incluso uno que no hizo acquire(). A veces es útil, pero es fácil equivocarse: mantén la disciplina.
¿Semaphore con un solo permiso = mutex?
Casi. Pero en un semáforo no existe el concepto de «propietario»: cualquier liberación incrementa el contador de permisos. En un mutex, debe liberar quien lo adquirió.
No confundas semáforo y pool
Un semáforo no es un pool de objetos, sino solo un «contador de permisos». A menudo se usa para implementar pools (por ejemplo, un pool de conexiones a la BD), pero por sí mismo no almacena nada.
6. Errores típicos al trabajar con mutex y semáforos
Error n.º 1: Te olvidaste de llamar a unlock/release. Si capturas un mutex o un semáforo pero no llamas a unlock() o release(), otros hilos pueden quedarse bloqueados para siempre. Usa siempre try-finally para garantizar la liberación incluso ante excepciones.
Error n.º 2: Sincronización sobre el objeto incorrecto. Si te sincronizas sobre una variable que no es común a todos los hilos (por ejemplo, una variable local o un literal de cadena), la sincronización no funcionará.
Error n.º 3: Doble liberación. En el caso de un semáforo: si llamas a release() más veces de las que hubo acquire(), la cantidad de permisos aumentará por encima del límite. ¡Vigila el balance!
Error n.º 4: Usar un semáforo en lugar de un mutex (o viceversa). Si necesitas acceso exclusivo, usa un mutex (synchronized o Lock). Si necesitas limitar el número de hilos que trabajan simultáneamente, usa Semaphore.
Error n.º 5: Mantener el bloqueo demasiado tiempo. Cuanto más tiempo un hilo retenga un mutex o un semáforo, más tiempo esperarán los demás. Minimiza el tiempo de trabajo dentro de la sección crítica.
GO TO FULL VERSION