Usando un objeto Mutex en Java

1. Información general

En este tutorial, veremos diferentes formas de implementar un mutex en Java .

2. Mutex

En una aplicación multiproceso, es posible que dos o más subprocesos necesiten acceder a un recurso compartido al mismo tiempo, lo que genera un comportamiento inesperado. Ejemplos de tales recursos compartidos son estructuras de datos, dispositivos de entrada y salida, archivos y conexiones de red.

Llamamos a este escenario una condición de carrera . Y la parte del programa que accede al recurso compartido se conoce como sección crítica . Entonces, para evitar una condición de carrera, necesitamos sincronizar el acceso a la sección crítica.

Un mutex (o exclusión mutua) es el tipo más simple de sincronizador - que asegura que sólo un hilo puede ejecutar la sección crítica de un programa de ordenador en un momento .

Para acceder a una sección crítica, un hilo adquiere el mutex, luego accede a la sección crítica y finalmente libera el mutex. Mientras tanto, todos los demás subprocesos se bloquean hasta que se libera el mutex. Tan pronto como un hilo sale de la sección crítica, otro hilo puede entrar en la sección crítica.

3. ¿Por qué Mutex?

Primero, tomemos un ejemplo de una clase SequenceGeneraror , que genera la siguiente secuencia incrementando el valor actual en uno cada vez:

public class SequenceGenerator { private int currentValue = 0; public int getNextSequence() { currentValue = currentValue + 1; return currentValue; } }

Ahora, creemos un caso de prueba para ver cómo se comporta este método cuando varios subprocesos intentan acceder a él simultáneamente:

@Test public void givenUnsafeSequenceGenerator_whenRaceCondition_thenUnexpectedBehavior() throws Exception { int count = 1000; Set uniqueSequences = getUniqueSequences(new SequenceGenerator(), count); Assert.assertEquals(count, uniqueSequences.size()); } private Set getUniqueSequences(SequenceGenerator generator, int count) throws Exception { ExecutorService executor = Executors.newFixedThreadPool(3); Set uniqueSequences = new LinkedHashSet(); List
    
      futures = new ArrayList(); for (int i = 0; i < count; i++) { futures.add(executor.submit(generator::getNextSequence)); } for (Future future : futures) { uniqueSequences.add(future.get()); } executor.awaitTermination(1, TimeUnit.SECONDS); executor.shutdown(); return uniqueSequences; }
    

Una vez que ejecutamos este caso de prueba, podemos ver que falla la mayor parte del tiempo con una razón similar a:

java.lang.AssertionError: expected: but was: at org.junit.Assert.fail(Assert.java:88) at org.junit.Assert.failNotEquals(Assert.java:834) at org.junit.Assert.assertEquals(Assert.java:645)

Se supone que uniqueSequences tiene un tamaño igual al número de veces que hemos ejecutado el método getNextSequence en nuestro caso de prueba. Sin embargo, este no es el caso debido a la condición de carrera. Obviamente, no queremos este comportamiento.

Entonces, para evitar tales condiciones de carrera, debemos asegurarnos de que solo un hilo pueda ejecutar el método getNextSequence a la vez . En tales escenarios, podemos usar un mutex para sincronizar los hilos.

Hay varias formas de implementar un mutex en Java. Entonces, a continuación, veremos las diferentes formas de implementar un mutex para nuestra clase SequenceGenerator .

4. Uso de palabras clave sincronizadas

Primero, discutiremos la palabra clave sincronizada , que es la forma más sencilla de implementar un mutex en Java.

Cada objeto en Java tiene un bloqueo intrínseco asociado. El método sincronizado y el bloque sincronizado utilizan este bloqueo intrínseco para restringir el acceso de la sección crítica a solo un subproceso a la vez.

Por tanto, cuando un hilo invoca un método sincronizado o entra en un bloque sincronizado , adquiere automáticamente el bloqueo. El bloqueo se libera cuando el método o bloque se completa o cuando se lanza una excepción.

Cambiemos getNextSequence para tener un mutex, simplemente agregando la palabra clave sincronizada :

public class SequenceGeneratorUsingSynchronizedMethod extends SequenceGenerator { @Override public synchronized int getNextSequence() { return super.getNextSequence(); } }

El bloque sincronizado es similar al método sincronizado , con más control sobre la sección crítica y el objeto que podemos usar para bloquear.

Por lo tanto, ahora vamos a ver cómo podemos utilizar el sincronizado de bloques para sincronizar en un objeto mutex encargo :

public class SequenceGeneratorUsingSynchronizedBlock extends SequenceGenerator { private Object mutex = new Object(); @Override public int getNextSequence() { synchronized (mutex) { return super.getNextSequence(); } } }

5. Uso de ReentrantLock

La clase ReentrantLock se introdujo en Java 1.5. Proporciona más flexibilidad y control que el enfoque de palabras clave sincronizadas .

Veamos cómo podemos usar ReentrantLock para lograr la exclusión mutua:

public class SequenceGeneratorUsingReentrantLock extends SequenceGenerator { private ReentrantLock mutex = new ReentrantLock(); @Override public int getNextSequence() { try { mutex.lock(); return super.getNextSequence(); } finally { mutex.unlock(); } } }

6. Usando Semaphore

Al igual que ReentrantLock , la clase Semaphore también se introdujo en Java 1.5.

Mientras que en el caso de un mutex, solo un hilo puede acceder a una sección crítica, Semaphore permite que un número fijo de hilos acceda a una sección crítica . Por lo tanto, también podemos implementar un mutex estableciendo el número de subprocesos permitidos en un semáforo en uno .

Let's now create another thread-safe version of SequenceGenerator using Semaphore:

public class SequenceGeneratorUsingSemaphore extends SequenceGenerator { private Semaphore mutex = new Semaphore(1); @Override public int getNextSequence() { try { mutex.acquire(); return super.getNextSequence(); } catch (InterruptedException e) { // exception handling code } finally { mutex.release(); } } }

7. Using Guava's Monitor Class

So far, we've seen the options to implement mutex using features provided by Java.

However, the Monitor class of Google's Guava library is a better alternative to the ReentrantLock class. As per its documentation, code using Monitor is more readable and less error-prone than the code using ReentrantLock.

First, we'll add the Maven dependency for Guava:

 com.google.guava guava 28.0-jre 

Now, we'll write another subclass of SequenceGenerator using the Monitor class:

public class SequenceGeneratorUsingMonitor extends SequenceGenerator { private Monitor mutex = new Monitor(); @Override public int getNextSequence() { mutex.enter(); try { return super.getNextSequence(); } finally { mutex.leave(); } } }

8. Conclusion

En este tutorial, analizamos el concepto de mutex. Además, hemos visto las diferentes formas de implementarlo en Java.

Como siempre, el código fuente completo de los ejemplos de código utilizados en este tutorial está disponible en GitHub.