Introducción a la sincronización en Java

La sincronización es una característica de Java que restringe que varios subprocesos intenten acceder a los recursos comúnmente compartidos al mismo tiempo. Aquí los recursos compartidos se refieren a contenidos de archivos externos, variables de clase o registros de bases de datos.

La sincronización se usa ampliamente en la programación multiproceso. "Sincronizado" es la palabra clave que proporciona a su código la capacidad de permitir que un solo hilo opere en él sin interferencia de ningún otro hilo durante ese período.

¿Por qué necesitamos sincronización en Java?

  • Java es un lenguaje de programación multiproceso. Esto significa que dos o más hilos pueden ejecutarse simultáneamente hacia la finalización de una tarea. Cuando los subprocesos se ejecutan simultáneamente, hay grandes posibilidades de que ocurra un escenario en el que su código pueda proporcionar resultados inesperados.
  • Quizás se pregunte si el subprocesamiento múltiple puede generar resultados erróneos, ¿por qué se considera una característica importante en Java?
  • Multithreading hace que su código sea más rápido al ejecutar múltiples subprocesos en paralelo y, por lo tanto, reduce el tiempo de ejecución de sus códigos y proporciona un alto rendimiento. Sin embargo, hacer uso del entorno de subprocesos múltiples conduce a resultados imprecisos debido a una condición comúnmente conocida como condición de carrera.

¿Qué es una condición de carrera?

Cuando dos o más subprocesos se ejecutan en paralelo, tienden a acceder y modificar los recursos compartidos en ese momento. Las secuencias en las que se ejecutan los hilos son decididas por el algoritmo de programación de hilos.

Debido a esto, no se puede predecir el orden en que se ejecutarán los subprocesos, ya que es controlado únicamente por el planificador de subprocesos. Esto afecta la salida del código y da como resultado salidas inconsistentes. Dado que varios subprocesos compiten entre sí para completar la operación, la condición se conoce como "condición de carrera".

Por ejemplo, consideremos el siguiente código:

Class Modify:
package JavaConcepts;
public class Modify implements Runnable(
private int myVar=0;
public int getMyVar() (
return myVar;
)
public void setMyVar(int myVar) (
this.myVar = myVar;
)
public void increment() (
myVar++;
)
@Override
public void run() (
// TODO Auto-generated method stub
this.increment();
System.out.println("Current thread being executed "+ Thread.currentThread().getName() + "Current Thread value " + this.getMyVar());
)
)
Class RaceCondition:
package JavaConcepts;
public class RaceCondition (
public static void main(String() args) (
Modify mObj = new Modify();
Thread t1 = new Thread(mObj, "thread 1");
Thread t2 = new Thread(mObj, "thread 2");
Thread t3 = new Thread(mObj, "thread 3");
t1.start();
t2.start();
t3.start();
)
)

Al ejecutar consecutivamente el código anterior, las salidas serán las siguientes:

Ourput1:

Hilo actual que se está ejecutando hilo 1 Valor de hilo actual 3

Subproceso actual que se ejecuta subproceso 3 Valor actual del subproceso 2

Subproceso actual que se ejecuta subproceso 2 Valor actual del subproceso 3

Salida2:

Subproceso actual que se ejecuta subproceso 3 Valor actual del subproceso 3

Subproceso actual que se ejecuta subproceso 2 Valor actual del subproceso 3

Hilo actual que se está ejecutando hilo 1 Valor de hilo actual 3

Salida3:

Subproceso actual que se ejecuta subproceso 2 Valor actual del subproceso 3

Hilo actual que se está ejecutando hilo 1 Valor de hilo actual 3

Subproceso actual que se ejecuta subproceso 3 Valor actual del subproceso 3

Salida4:

Subproceso actual que se ejecuta subproceso 1 Valor actual del subproceso 2

Subproceso actual que se ejecuta subproceso 3 Valor actual del subproceso 3

Subproceso actual que se ejecuta subproceso 2 Valor actual del subproceso 2

  • Del ejemplo anterior, puede concluir que los subprocesos se ejecutan al azar y que el valor es incorrecto. Según nuestra lógica, el valor debe incrementarse en 1. Sin embargo, aquí el valor de salida en la mayoría de los casos es 3 y en algunos casos, es 2.
  • Aquí la variable "myVar" es el recurso compartido en el que se ejecutan varios subprocesos. Los hilos están accediendo y modificando el valor de "myVar" simultáneamente. Veamos qué sucede si comentamos los otros dos hilos.

La salida en este caso es:

El hilo actual que se está ejecutando hilo 1 Valor actual del hilo 1

Esto significa que cuando se ejecuta un solo subproceso, la salida es la esperada. Sin embargo, cuando se ejecutan varios subprocesos, cada subproceso modifica el valor. Por lo tanto, uno necesita restringir el número de hilos que trabajan en un recurso compartido a un solo hilo a la vez. Esto se logra mediante la sincronización.

Comprender qué es la sincronización en Java

  • La sincronización en Java se logra con la ayuda de la palabra clave "sincronizado". Esta palabra clave se puede usar para métodos o bloques u objetos, pero no se puede usar con clases y variables. Un código sincronizado permite que solo un hilo acceda y lo modifique en un momento dado.
  • Sin embargo, un código sincronizado afecta el rendimiento del código ya que aumenta el tiempo de espera de otros hilos que intentan acceder a él. Por lo tanto, un fragmento de código debe sincronizarse solo cuando existe la posibilidad de que se produzca una condición de carrera. Si no, uno debería evitarlo.

¿Cómo funciona la sincronización en Java internamente?

  • La sincronización interna en Java se ha implementado con la ayuda del concepto de bloqueo (también conocido como monitor). Cada objeto Java tiene su propio bloqueo. En un bloque de código sincronizado, un subproceso necesita adquirir el bloqueo antes de poder ejecutar ese bloque de código en particular. Una vez que un subproceso adquiere el bloqueo, puede ejecutar ese fragmento de código.
  • Al finalizar la ejecución, libera automáticamente el bloqueo. Si otro subproceso requiere operar en el código sincronizado, espera que el subproceso actual que opera en él libere el bloqueo. La máquina virtual Java se encarga internamente de este proceso de adquisición y liberación de bloqueos. Un programa no es responsable de adquirir y liberar bloqueos por el hilo. Sin embargo, los subprocesos restantes pueden ejecutar cualquier otro fragmento de código no sincronizado simultáneamente.

Sincronicemos nuestro ejemplo anterior sincronizando el código dentro del método de ejecución utilizando el bloque sincronizado en la clase "Modificar" como se muestra a continuación:

Class Modify:
package JavaConcepts;
public class Modify implements Runnable(
private int myVar=0;
public int getMyVar() (
return myVar;
)
public void setMyVar(int myVar) (
this.myVar = myVar;
)
public void increment() (
myVar++;
)
@Override
public void run() (
// TODO Auto-generated method stub
synchronized(this) (
this.increment();
System.out.println("Current thread being executed "
+ Thread.currentThread().getName() + " Current Thread value " + this.getMyVar());
)
)
)

El código para la clase "RaceCondition" sigue siendo el mismo. Ahora al ejecutar el código, la salida es la siguiente:

Salida1:

El hilo actual que se está ejecutando hilo 1 Valor actual del hilo 1

El hilo actual que se está ejecutando hilo 2 Valor actual del hilo 2

El hilo actual que se está ejecutando hilo 3 Valor actual del hilo 3

Salida2:

El hilo actual que se está ejecutando hilo 1 Valor actual del hilo 1

El hilo actual que se está ejecutando hilo 3 Valor actual del hilo 2

El hilo actual que se está ejecutando hilo 2 Valor actual del hilo 3

Tenga en cuenta que nuestro código proporciona el resultado esperado. Aquí cada hilo está incrementando el valor en 1 para la variable "myVar" (en la clase "Modificar").

Nota: Se requiere sincronización cuando varios subprocesos están operando en el mismo objeto. Si hay varios subprocesos operando en varios objetos, entonces no se requiere sincronización.

Por ejemplo, modifiquemos el código en la clase "RaceCondition" como se muestra a continuación y trabajemos con la clase previamente no sincronizada "Modificar".

package JavaConcepts;
public class RaceCondition (
public static void main(String() args) (
Modify mObj = new Modify();
Modify mObj1 = new Modify();
Modify mObj2 = new Modify();
Thread t1 = new Thread(mObj, "thread 1");
Thread t2 = new Thread(mObj1, "thread 2");
Thread t3 = new Thread(mObj2, "thread 3");
t1.start();
t2.start();
t3.start();
)
)

Salida:

El hilo actual que se está ejecutando hilo 1 Valor actual del hilo 1

El hilo actual que se está ejecutando hilo 2 Valor actual del hilo 1

El hilo actual que se está ejecutando hilo 3 Valor actual del hilo 1

Tipos de sincronización en Java:

Hay dos tipos de sincronización de subprocesos, uno que es mutuamente excluyente y el otro comunicación entre subprocesos.

1.Mutualmente exclusivo

  • Método sincronizado
  • Método estático sincronizado
  • Bloque sincronizado.

2. Coordinación de subprocesos (comunicación entre subprocesos en java)

Mutuamente excluyentes:

  • En este caso, los subprocesos obtienen el bloqueo antes de operar en un objeto, evitando así trabajar con objetos que han tenido sus valores manipulados por otros subprocesos.
  • Esto se puede lograr de tres maneras:

yo. Método sincronizado: Podemos utilizar la palabra clave "sincronizada" para un método, convirtiéndolo en un método sincronizado. Cada subproceso que invoca el método sincronizado obtendrá el bloqueo para ese objeto y lo liberará una vez que se complete su operación. En el ejemplo anterior, podemos hacer que nuestro método "run ()" esté sincronizado mediante el uso de la palabra clave "sincronizado" después del modificador de acceso.

@Override
public synchronized void run() (
// TODO Auto-generated method stub
this.increment();
System.out.println("Current thread being executed "
+ Thread.currentThread().getName() + " Current Thread value " + this.getMyVar());
)

El resultado para este caso será:

El hilo actual que se está ejecutando hilo 1 Valor actual del hilo 1

El hilo actual que se está ejecutando hilo 3 Valor actual del hilo 2

El hilo actual que se está ejecutando hilo 2 Valor actual del hilo 3

ii) Método sincronizado estático: para sincronizar métodos estáticos, uno necesita adquirir su bloqueo de nivel de clase. Después de que un subproceso obtenga el bloqueo de nivel de clase solo entonces podrá ejecutar un método estático. Mientras que un hilo mantiene el bloqueo de nivel de clase, ningún otro hilo puede ejecutar ningún otro método estático sincronizado de esa clase. Sin embargo, los otros subprocesos pueden ejecutar cualquier otro método regular o método estático regular o incluso un método sincronizado no estático de esa clase.

Por ejemplo, consideremos nuestra clase "Modificar" y realicemos cambios al convertir nuestro método de "incremento" a un método estático sincronizado. Los cambios en el código son los siguientes:

package JavaConcepts;
public class Modify implements Runnable(
private static int myVar=0;
public int getMyVar() (
return myVar;
)
public void setMyVar(int myVar) (
this.myVar = myVar;
)
public static synchronized void increment() (
myVar++;
System.out.println("Current thread being executed " + Thread.currentThread().getName() + " Current Thread value " + myVar);
)
@Override
public void run() (
// TODO Auto-generated method stub
increment();
)
)

iii) Bloque sincronizado: una de las principales desventajas del método sincronizado es que aumenta el tiempo de espera de los subprocesos y afecta el rendimiento del código. Por lo tanto, para poder sincronizar solo las líneas de código requeridas en lugar de todo el método, es necesario utilizar un bloque sincronizado. El uso del bloqueo sincronizado reduce el tiempo de espera de los subprocesos y también mejora el rendimiento. En el ejemplo anterior, ya hemos utilizado el bloque sincronizado mientras sincronizamos nuestro código por primera vez.

Ejemplo:
public void run() (
// TODO Auto-generated method stub
synchronized(this) (
this.increment();
System.out.println("Current thread being executed "
+ Thread.currentThread().getName() + " Current Thread value " + this.getMyVar());
)
)

Coordinación de hilos:

Para hilos sincronizados, la comunicación entre hilos es una tarea importante. Los métodos incorporados que ayudan a lograr la comunicación entre subprocesos para el código sincronizado son:

  • Espere()
  • notificar()
  • notifyAll ()

Nota: Estos métodos pertenecen a la clase de objeto y no a la clase de hilo. Para que un subproceso pueda invocar estos métodos en un objeto, debe mantener el bloqueo en ese objeto. Además, estos métodos hacen que un subproceso libere su bloqueo en el objeto en el que se invoca.

wait (): un subproceso al invocar el método wait (), libera el bloqueo del objeto y pasa al estado de espera. Tiene dos sobrecargas de métodos:

  • public final void wait () arroja InterruptedException
  • público final nulo espera (largo tiempo de espera) lanza InterruptedException
  • público final nulo espera (tiempo de espera largo, int nanos) arroja InterruptedException

notify (): un subproceso envía una señal a otro subproceso en el estado de espera mediante el uso del método notify (). Envía la notificación a solo un subproceso de modo que este subproceso pueda reanudar su ejecución. El subproceso que recibirá la notificación entre todos los subprocesos en estado de espera depende de la máquina virtual Java.

  • público final nulo notificar ()

notifyAll (): cuando un hilo invoca el método notifyAll (), se notifica a cada hilo en su estado de espera. Estos subprocesos se ejecutarán uno tras otro en función del orden decidido por la máquina virtual Java.

  • público final nulo notifyAll ()

Conclusión

En este artículo, hemos visto cómo trabajar en un entorno de subprocesos múltiples puede generar inconsistencias de datos debido a una condición de carrera. Cómo la sincronización nos ayuda a superar esto al limitar un solo hilo para operar en un recurso compartido a la vez. También cómo los hilos sincronizados se comunican entre sí.

Artículos recomendados:

Esta ha sido una guía de ¿Qué es la sincronización en Java? Aquí discutimos la introducción, comprensión, necesidad, funcionamiento y tipos de sincronización con algún código de muestra. También puede consultar nuestros otros artículos sugeridos para obtener más información:

  1. Serialización en Java
  2. ¿Qué es genéricos en Java?
  3. ¿Qué es la API en Java?
  4. ¿Qué es un árbol binario en Java?
  5. Ejemplos y cómo funcionan los genéricos en C #