Guía para la palabra clave sincronizada en Java

1. Información general

Este artículo rápido será una introducción al uso del bloque sincronizado en Java.

En pocas palabras, en un entorno de subprocesos múltiples, se produce una condición de carrera cuando dos o más subprocesos intentan actualizar datos compartidos mutables al mismo tiempo. Java ofrece un mecanismo para evitar condiciones de carrera sincronizando el acceso a los subprocesos con los datos compartidos.

Una pieza de lógica marcada con sincronizada se convierte en un bloque sincronizado, lo que permite que solo se ejecute un hilo en un momento dado .

2. ¿Por qué la sincronización?

Consideremos una condición de carrera típica en la que calculamos la suma y varios subprocesos ejecutan el método calculate () :

public class BaeldungSynchronizedMethods { private int sum = 0; public void calculate() { setSum(getSum() + 1); } // standard setters and getters } 

Y escribamos una prueba simple:

@Test public void givenMultiThread_whenNonSyncMethod() { ExecutorService service = Executors.newFixedThreadPool(3); BaeldungSynchronizedMethods summation = new BaeldungSynchronizedMethods(); IntStream.range(0, 1000) .forEach(count -> service.submit(summation::calculate)); service.awaitTermination(1000, TimeUnit.MILLISECONDS); assertEquals(1000, summation.getSum()); }

Simplemente estamos usando un ExecutorService con un grupo de 3 subprocesos para ejecutar el cálculo () 1000 veces.

Si ejecutamos esto en serie, la salida esperada sería 1000, pero nuestra ejecución multiproceso falla casi todas las veces con una salida real inconsistente, por ejemplo:

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

Este resultado, por supuesto, no es inesperado.

Una forma sencilla de evitar la condición de carrera es hacer que la operación sea segura para subprocesos utilizando la palabra clave sincronizada .

3. La palabra clave sincronizada

La palabra clave sincronizada se puede utilizar en diferentes niveles:

  • Métodos de instancia
  • Métodos estáticos
  • Bloques de código

Cuando usamos un bloque sincronizado , internamente Java usa un monitor también conocido como bloqueo de monitor o bloqueo intrínseco, para proporcionar sincronización. Estos monitores están vinculados a un objeto, por lo que todos los bloques sincronizados del mismo objeto pueden tener solo un hilo ejecutándolos al mismo tiempo.

3.1. Métodos de instancia sincronizados

Simplemente agregue la palabra clave sincronizada en la declaración del método para sincronizar el método:

public synchronized void synchronisedCalculate() { setSum(getSum() + 1); }

Observe que una vez que sincronizamos el método, el caso de prueba pasa, con la salida real como 1000:

@Test public void givenMultiThread_whenMethodSync() { ExecutorService service = Executors.newFixedThreadPool(3); SynchronizedMethods method = new SynchronizedMethods(); IntStream.range(0, 1000) .forEach(count -> service.submit(method::synchronisedCalculate)); service.awaitTermination(1000, TimeUnit.MILLISECONDS); assertEquals(1000, method.getSum()); }

Los métodos de instancia se sincronizan sobre la instancia de la clase que posee el método. Lo que significa que solo un hilo por instancia de la clase puede ejecutar este método.

3.2. Sincronizada Stati c Métodos

Los métodos estáticos se sincronizan como los métodos de instancia:

 public static synchronized void syncStaticCalculate() { staticSum = staticSum + 1; }

Estos métodos se sincronizan en el objeto Class asociado con la clase y dado que solo existe un objeto Class por JVM por clase, solo un hilo puede ejecutarse dentro de un método sincronizado estático por clase, independientemente del número de instancias que tenga.

Probémoslo:

@Test public void givenMultiThread_whenStaticSyncMethod() { ExecutorService service = Executors.newCachedThreadPool(); IntStream.range(0, 1000) .forEach(count -> service.submit(BaeldungSynchronizedMethods::syncStaticCalculate)); service.awaitTermination(100, TimeUnit.MILLISECONDS); assertEquals(1000, BaeldungSynchronizedMethods.staticSum); }

3.3. Bloques sincronizados dentro de métodos

A veces no queremos sincronizar todo el método sino solo algunas instrucciones dentro de él. Esto se puede lograr aplicando sincronizado a un bloque:

public void performSynchronisedTask() { synchronized (this) { setCount(getCount()+1); } }

Probemos el cambio:

@Test public void givenMultiThread_whenBlockSync() { ExecutorService service = Executors.newFixedThreadPool(3); BaeldungSynchronizedBlocks synchronizedBlocks = new BaeldungSynchronizedBlocks(); IntStream.range(0, 1000) .forEach(count -> service.submit(synchronizedBlocks::performSynchronisedTask)); service.awaitTermination(100, TimeUnit.MILLISECONDS); assertEquals(1000, synchronizedBlocks.getCount()); }

Observe que pasamos un parámetro this al bloque sincronizado . Este es el objeto monitor, el código dentro del bloque se sincroniza en el objeto monitor. En pocas palabras, solo un hilo por objeto de monitor se puede ejecutar dentro de ese bloque de código.

En caso de que el método sea estático , pasaríamos el nombre de la clase en lugar de la referencia del objeto. Y la clase sería un monitor para la sincronización del bloque:

public static void performStaticSyncTask(){ synchronized (SynchronisedBlocks.class) { setStaticCount(getStaticCount() + 1); } }

Probemos el bloque dentro del método estático :

@Test public void givenMultiThread_whenStaticSyncBlock() { ExecutorService service = Executors.newCachedThreadPool(); IntStream.range(0, 1000) .forEach(count -> service.submit(BaeldungSynchronizedBlocks::performStaticSyncTask)); service.awaitTermination(100, TimeUnit.MILLISECONDS); assertEquals(1000, BaeldungSynchronizedBlocks.getStaticCount()); }

3.4. Reentrada

El bloqueo detrás de los métodos y bloques sincronizados es reentrante. Es decir, el hilo actual puede adquirir el mismo bloqueo sincronizado una y otra vez mientras lo mantiene presionado:

Object lock = new Object(); synchronized (lock) { System.out.println("First time acquiring it"); synchronized (lock) { System.out.println("Entering again"); synchronized (lock) { System.out.println("And again"); } } }

Como se muestra arriba, mientras estamos en un bloque sincronizado , podemos adquirir el mismo bloqueo de monitor repetidamente.

4. Conclusión

En este artículo rápido, hemos visto diferentes formas de usar la palabra clave sincronizada para lograr la sincronización de subprocesos.

También exploramos cómo una condición de carrera puede afectar nuestra aplicación y cómo la sincronización nos ayuda a evitar eso. Para obtener más información sobre la seguridad de subprocesos utilizando bloqueos en Java, consulte nuestro artículo java.util.concurrent.Locks .

El código completo de este tutorial está disponible en GitHub.