Semáforos en Java
En Java, un semáforo es una herramienta de sincronización que se utiliza para controlar el acceso a recursos compartidos en un entorno concurrente. Un semáforo mantiene un contador que representa el número de permisos disponibles para acceder a un recurso. Los hilos pueden adquirir o liberar permisos del semáforo para acceder al recurso compartido.
En Java, la clase Semaphore del paquete java.util.concurrent se utiliza para implementar semáforos. A continuación, se muestra un ejemplo de cómo utilizar un semáforo para controlar el acceso a un recurso compartido:
public class Worker implements Runnable {
private Semaphore semaphore;
public Worker(Semaphore semaphore) {
this.semaphore = semaphore;
}
@Override
public void run() {
try {
// Adquirir un permiso del semáforo
semaphore.acquire();
System.out.println("Worker " + Thread.currentThread().getName() + " is working...");
Thread.sleep(2000); // Simular trabajo
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// Liberar el permiso del semáforo
semaphore.release();
}
}
}
En este ejemplo, la clase Worker implementa la interfaz Runnable y utiliza un semáforo para controlar el acceso a un recurso compartido. En el método run(), el hilo adquiere un permiso del semáforo antes de realizar su trabajo y lo libera después de completar su tarea.
Para utilizar la clase Worker, podemos crear un semáforo con un número específico de permisos y luego iniciar varios hilos que intenten adquirir esos permisos:
void main(String[] args) {
Semaphore semaphore = new Semaphore(3); // Permitir hasta 3 hilos al mismo tiempo
for (int i = 0; i < 10; i++) {
new Thread(new Worker(semaphore)).start();
}
}
En este ejemplo, se crea un semáforo con 3 permisos, lo que significa que hasta 3 hilos pueden acceder al recurso compartido al mismo tiempo. Al iniciar 10 hilos, algunos de ellos tendrán que esperar hasta que otros liberen sus permisos para poder acceder al recurso compartido.
Funciones del semáforo
acquire(): Adquiere un permiso del semáforo. Si no hay permisos disponibles, el hilo se bloquea hasta que un permiso esté disponible.release(): Libera un permiso del semáforo, permitiendo que otros hilos puedan adquirirlo.availablePermits(): Devuelve el número de permisos disponibles en el semáforo.tryAcquire(): Intenta adquirir un permiso del semáforo sin bloquear el hilo. Devuelvetruesi se adquirió un permiso, ofalsesi no hay permisos disponibles.tryAcquire(long timeout, TimeUnit unit): Intenta adquirir un permiso del semáforo, bloqueando el hilo durante un tiempo máximo especificado. Devuelvetruesi se adquirió un permiso, ofalsesi no se adquirió dentro del tiempo especificado.drainPermits(): Elimina todos los permisos disponibles del semáforo y devuelve el número de permisos eliminados.
Exclusión mutua con semáforos
Los semáforos también se pueden utilizar para implementar exclusión mutua, lo que significa que solo un hilo puede acceder a un recurso compartido en un momento dado. Para lograr esto, se puede crear un semáforo con un solo permiso:
Semaphore mutex = new Semaphore(1); // Semáforo de exclusión mutua
En este caso, solo un hilo podrá adquirir el permiso del semáforo y acceder al recurso compartido, mientras que los demás hilos tendrán que esperar hasta que el permiso sea liberado.
Sincronización Genérica con semáforos
Además de la exclusión mutua, los semáforos también se pueden utilizar para sincronizar hilos de manera más general. Por ejemplo, se pueden utilizar para implementar un sistema de productores y consumidores, donde los productores generan datos y los consumidores los consumen. En este caso, se pueden utilizar dos semáforos: uno para controlar el acceso a un buffer compartido y otro para contar el número de elementos en el buffer.
Semaphore empty = new Semaphore(10); // Semáforo para contar los espacios vacíos en el buffer
Semaphore full = new Semaphore(0); // Semáforo para contar los elementos en el buffer
Semaphore mutex = new Semaphore(1); // Semáforo de exclusión mutua
En este ejemplo, el semáforo empty se utiliza para contar el número de espacios vacíos en el buffer, mientras que el semáforo full se utiliza para contar el número de elementos en el buffer. El semáforo mutex se utiliza para garantizar la exclusión mutua al acceder al buffer compartido.
Ejemplo de Productores y Consumidores con Semáforos
A continuación, se muestra un ejemplo de cómo implementar un sistema de productores y consumidores utilizando semáforos en Java:
public class Producer implements Runnable {
private Semaphore empty;
private Semaphore full;
private Semaphore mutex;
private Buffer buffer;
public Producer(Semaphore empty, Semaphore full, Semaphore mutex, Buffer buffer) {
this.empty = empty;
this.full = full;
this.mutex = mutex;
this.buffer = buffer;
}
@Override
public void run() {
try {
while (true) {
empty.acquire(); // Esperar a que haya espacio en el buffer
mutex.acquire(); // Adquirir exclusión mutua
buffer.add(); // Agregar un elemento al buffer
mutex.release(); // Liberar exclusión mutua
full.release(); // Incrementar el contador de elementos en el buffer
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class Consumer implements Runnable {
private Semaphore empty;
private Semaphore full;
private Semaphore mutex;
private Buffer buffer;
public Consumer(Semaphore empty, Semaphore full, Semaphore mutex, Buffer buffer) {
this.empty = empty;
this.full = full;
this.mutex = mutex;
this.buffer = buffer;
}
@Override
public void run() {
try {
while (true) {
full.acquire(); // Esperar a que haya elementos en el buffer
mutex.acquire(); // Adquirir exclusión mutua
buffer.remove(); // Eliminar un elemento del buffer
mutex.release(); // Liberar exclusión mutua
empty.release(); // Incrementar el contador de espacios vacíos en el buffer
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
En este ejemplo, la clase Producer representa a los productores que generan datos y los agregan al buffer compartido, mientras que la clase Consumer representa a los consumidores que consumen los datos del buffer. Los semáforos se utilizan para sincronizar el acceso al buffer y garantizar que los productores y consumidores trabajen de manera coordinada.
Semáforos Binarios
Un semáforo binario es un tipo especial de semáforo que solo tiene dos estados: 0 y 1. Se utiliza para implementar exclusión mutua, donde solo un hilo puede acceder a un recurso compartido en un momento dado. Un semáforo binario se puede crear utilizando la clase Semaphore con un solo permiso:
Semaphore binarySemaphore = new Semaphore(1); // Semáforo binario
En este caso, el semáforo binarySemaphore se comportará como un semáforo binario, permitiendo que solo un hilo acceda al recurso compartido a la vez.
Semáforos Fuertes y Débiles
Los semáforos pueden ser fuertes o débiles dependiendo de cómo se manejen los hilos que intentan adquirir permisos. Un semáforo fuerte garantiza que los hilos adquieran permisos en el orden en que los solicitaron, mientras que un semáforo débil no garantiza ningún orden específico. En Java, la clase Semaphore implementa un semáforo fuerte por defecto, pero también se puede crear un semáforo débil utilizando el constructor Semaphore(int permits, boolean fair) con el parámetro fair establecido en false.
Semaphore weakSemaphore = new Semaphore(3, false); // Semáforo débil
En este caso, el semáforo weakSemaphore se comportará como un semáforo débil, lo que significa que los hilos pueden adquirir permisos en cualquier orden, sin garantizar un orden específico.
Semáforos y Barreras de Sincronización
Una barrera de sincronización es un mecanismo que permite que un grupo de hilos se sincronice en un punto específico de su ejecución. Los semáforos se pueden utilizar para implementar barreras de sincronización, donde los hilos deben esperar hasta que todos los hilos hayan alcanzado la barrera antes de continuar con su ejecución.
public class Barrier {
private int count = 0;
private int totalThreads;
private Semaphore mutex = new Semaphore(1);
private Semaphore barrier = new Semaphore(0);
public Barrier(int totalThreads) {
this.totalThreads = totalThreads;
}
public void await() throws InterruptedException {
mutex.acquire();
count++;
if (count == totalThreads) {
barrier.release(totalThreads); // Liberar a todos los hilos en la barrera
}
mutex.release();
barrier.acquire(); // Esperar a que todos los hilos lleguen a la barrera
}
}
En este ejemplo, la clase Barrier implementa una barrera de sincronización utilizando semáforos. El método await() se llama por cada hilo que llega a la barrera, y el semáforo barrier se libera solo cuando todos los hilos han llegado a la barrera, lo que permite que todos los hilos continúen con su ejecución.
Conclusión
Los semáforos son una herramienta poderosa para controlar el acceso a recursos compartidos en la programación concurrente. Al utilizar semáforos, podemos evitar condiciones de carrera y garantizar que los hilos accedan a los recursos de manera segura y coordinada. Es importante entender cómo funcionan los semáforos y cómo utilizarlos correctamente para evitar problemas de sincronización en nuestras aplicaciones concurrentes.
Emulando condiciones de carrera en Java
En este artículo se explicará cómo emular condiciones de carrera en Java, utilizando un ejemplo práctico para ilustrar el concepto y las consecuencias de las condiciones de carrera en la programación concurrente.
La clase Timer en Java Swing
En este artículo se explicará cómo utilizar la clase Timer en Java Swing para programar tareas que se ejecuten después de un cierto retraso o de forma periódica, lo que es útil para crear interfaces de usuario dinámicas y reactivas.