Programación concurrente

Semáforos en Java

En este artículo se explicará qué son los semáforos en Java, cómo funcionan y cómo se pueden utilizar para controlar el acceso a recursos compartidos en la programación concurrente.

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. Devuelve true si se adquirió un permiso, o false si 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. Devuelve true si se adquirió un permiso, o false si 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.

Copyright Jesús Aurelio Castro Magaña © 2026