Sincronización de Hilos en Java

Avanzado

Al usar múltiples hilos, a veces es necesario coordinar las actividades de dos o más. El proceso por el cual esto se logra se llama sincronización (synchronization). La razón más común para la sincronización es cuando dos o más hilos necesitan acceso a un recurso compartido que solo puede ser utilizado por un hilo a la vez.

Por ejemplo, cuando un hilo está escribiendo en un archivo, se debe evitar que un segundo hilo lo haga al mismo tiempo. Otra razón para la sincronización es cuando un hilo está esperando un evento causado por otro hilo. En este caso, debe haber algún medio por el cual el primer hilo se mantenga en estado suspendido hasta que ocurra el evento. Entonces, el hilo de espera debe reanudar la ejecución.

1. Conceptos sobre Sincronización

La clave para la sincronización en Java es el concepto de monitor, que controla el acceso a un objeto. Un monitor funciona implementando el concepto de bloqueo (lock). Cuando un objeto está bloqueado por un hilo, ningún otro hilo puede obtener acceso al objeto. Cuando el hilo sale, el objeto está desbloqueado y está disponible para ser utilizado por otro hilo.

Todos los objetos en Java tienen un monitor. Esta característica está integrada en el lenguaje Java en sí. Por lo tanto, todos los objetos se pueden sincronizar. La sincronización está respaldada por la palabra clave synchronized y algunos métodos bien definidos que tienen todos los objetos. Como la sincronización se diseñó en Java desde el principio, es mucho más fácil de usar de lo que se podría esperar. De hecho, para muchos programas, la sincronización de objetos es casi transparente.

Hay dos formas de sincronizar tu código. Ambos implican el uso de la palabra clave synchronized, y ambos se examinan aquí.

2. Usando métodos synchronized

Puede sincronizar el acceso a un método modificándolo con la palabra clave synchronized. Cuando se llama a ese método, el hilo de llamada entra en el monitor del objeto, que luego bloquea el objeto.

  • Mientras está bloqueado, ningún otro hilo puede ingresar al método, o ingresar cualquier otro método sincronizado definido por la clase del objeto.
  • Cuando el hilo retorna del método, el monitor desbloquea el objeto, permitiendo que sea utilizado por el siguiente hilo. Por lo tanto, la sincronización se logra con prácticamente ningún esfuerzo de programación de tu parte.

El siguiente programa muestra la sincronización al controlar el acceso a un método llamado sumArray(), que suma los elementos de una matriz de enteros.

//Uso de Sincronizacion para controlar el acceso.
class sumArray{
    private int sum;

    //sumArray está sincronizado
    synchronized int sumArray(int nums[]){
        sum=0;
        for (int i=0; i<nums.length;i++){
            sum+=nums[i];
            System.out.println("Total acumulado de "+Thread.currentThread().getName()+" es "+sum);
        try {
            Thread.sleep(10);//permitir el cambio de tarea
        }catch (InterruptedException exc){
            System.out.println("Hilo interrumpido");
        }
        }
        return sum;
    }
}

class MiHilo implements Runnable{
    Thread hilo;
    static sumArray sumarray= new sumArray();
    int a[];
    int resp;

    //Construye un nuevo hilo.
    MiHilo(String nombre, int nums[]){
        hilo= new Thread(this,nombre);
        a=nums;
    }

    //Un método que crea e inicia un hilo
    public static MiHilo creaEInicia (String nombre,int nums[]){
        MiHilo miHilo=new MiHilo(nombre,nums);

        miHilo.hilo.start(); //Inicia el hilo
        return miHilo;
    }
    //Punto de entrada del hilo
    public void run(){
        int sum;
        System.out.println(hilo.getName()+ " iniciando.");

        resp=sumarray.sumArray(a);
        System.out.println("Suma para "+hilo.getName()+ " es "+resp);
        System.out.println(hilo.getName()+ " terminado.");
    }
}
class Sincronizacion {
    public static void main(String[] args) {
        int a[]={1,2,3,4,5};
        MiHilo mh1 = MiHilo.creaEInicia("#1",a);
        MiHilo mh2 = MiHilo.creaEInicia("#2",a);

        try {
            mh1.hilo.join();
            mh2.hilo.join();
        }catch (InterruptedException exc){
            System.out.println("Hilo principal interrumpido.");
        }
    }
}

Salida: (La salida precisa puede diferir en tu computadora.)

#1 iniciando.
#2 iniciando.
Total acumulado de #1 es 1
Total acumulado de #1 es 3
Total acumulado de #1 es 6
Total acumulado de #1 es 10
Total acumulado de #1 es 15
Total acumulado de #2 es 1
Suma para #1 es 15
#1 terminado.
Total acumulado de #2 es 3
Total acumulado de #2 es 6
Total acumulado de #2 es 10
Total acumulado de #2 es 15
Suma para #2 es 15
#2 terminado.

2.1. Explicación del ejemplo

Examinemos este programa en detalle. El programa crea tres clases. El primero es SumArray. Contiene el método sumArray(), que suma una matriz de enteros. La segunda clase es MiHilo, que usa un objeto estático de tipo SumArray para obtener la suma de una matriz de enteros. Este objeto se llama sumarray y como es estático, solo hay una copia de él compartida por todas las instancias de MiHilo. Finalmente, la clase Sincronizacion crea dos hilos y cada uno calcula la suma de una matriz entera.

Dentro de sumArray(), sleep() se usa para permitir deliberadamente que se produzca un cambio de tarea, si se puede –pero no. Debido a que sumArray() está sincronizado, puede ser utilizado solo por un hilo a la vez. Por lo tanto, cuando el segundo hilo (hijo) comienza la ejecución, no ingresa sumArray() hasta después de que el primer hilo secundario haya terminado con él. Esto asegura que se produzca el resultado correcto.

Para comprender completamente los efectos de synchronized, intente eliminarlo de la declaración de sumArray(). Después de hacer esto, sumArray() ya no está sincronizado, y cualquier cantidad de hiloss puede usarlo al mismo tiempo.

2.2. Ejemplo sin el uso de synchronized

El problema con esto es que el total acumulado se almacena en sum, que se cambiará por cada hilo que invoque sumArray() a través del objeto estático sumarray. Por lo tanto, cuando dos hilos llaman a sumarray.sumArray() al mismo tiempo, se producen resultados incorrectos porque sum refleja la suma de ambos hilos, mezclados. Por ejemplo, aquí hay un resultado de muestra del programa después de que se haya eliminado synchronized de la declaración de sumArray(). (La salida precisa puede diferir en su computadora).

Salida:

#1 iniciando.
#2 iniciando.
Total acumulado de #2 es 1
Total acumulado de #1 es 1
Total acumulado de #2 es 3
Total acumulado de #1 es 5
Total acumulado de #2 es 8
Total acumulado de #1 es 11
Total acumulado de #2 es 15
Total acumulado de #1 es 19
Total acumulado de #2 es 24
Total acumulado de #1 es 29
Suma para #2 es 29
Suma para #1 es 29
#2 terminado.
#1 terminado.

Como muestra el resultado, ambos hilos hijo están llamando a sumarray.sumArray() al mismo tiempo, y el valor de sum está corrupto. Antes de continuar, repasemos los puntos clave de un método sincronizado (synchronized):

  1. Se crea un método sincronizado precediendo su declaración con la palabra clave synchronized.
  2. Para cualquier objeto dado, una vez que se ha llamado a un método sincronizado, el objeto está bloqueado y ningún otro método de ejecución puede utilizar métodos sincronizados en el mismo objeto.
  3. Otros hilos que intenten llamar a un objeto sincronizado en uso ingresarán en un estado de espera hasta que el objeto esté desbloqueado.
  4. Cuando un hilo sale del método sincronizado, el objeto se desbloquea.

3. La declaración synchronized

Si bien la creación de métodos synchronized dentro de las clases que usted crea es un medio fácil y efectivo para lograr la sincronización, no funcionará en todos los casos. Por ejemplo, es posible que desee sincronizar el acceso a algún método que no esté modificado por synchronized.

Esto puede ocurrir porque desea utilizar una clase que no haya creado usted, sino un tercero, y no tenga acceso al código fuente. Por lo tanto, no es posible agregar synchronized a los métodos apropiados dentro de la clase. ¿Cómo se puede sincronizar el acceso a un objeto de esta clase? Afortunadamente, la solución a este problema es bastante sencilla: simplemente realiza llamadas a los métodos definidos por esta clase dentro de un bloque synchronized.

Esta es la forma general de un bloque synchronized:

synchronized(objref) {
// declaraciones a sincronizar
}

Aquí, objref es una referencia al objeto que se sincroniza. Una vez que se ha ingresado un bloque sincronizado, ningún otro hilo puede llamar a un método sincronizado en el objeto referido por objref hasta que se haya salido del bloque.

Por ejemplo, otra forma de sincronizar llamadas a sumArray() es llamarlo desde un bloque synchronized, como se muestra en esta versión del programa:

//Uso de un bloque sincronizado para controlar el acceso a SumArray.
class sumArray{
    private int sum;

    //sumArray no está sincronizado
     int sumArray(int nums[]){
        sum=0;
        for (int i=0; i<nums.length;i++){
            sum+=nums[i];
            System.out.println("Total acumulado de "+Thread.currentThread().getName()+" es "+sum);
        try {
            Thread.sleep(10);//permitir el cambio de tarea
        }catch (InterruptedException exc){
            System.out.println("Hilo interrumpido");
        }
        }
        return sum;
    }
}

class MiHilo implements Runnable{
    Thread hilo;
    static sumArray sumarray= new sumArray();
    int a[];
    int resp;

    //Construye un nuevo hilo.
    MiHilo(String nombre, int nums[]){
        hilo= new Thread(this,nombre);
        a=nums;
    }

    //Un método que crea e inicia un hilo
    public static MiHilo creaEInicia (String nombre,int nums[]){
        MiHilo miHilo=new MiHilo(nombre,nums);

        miHilo.hilo.start(); //Inicia el hilo
        return miHilo;
    }
    //Punto de entrada del hilo
    public void run(){
        int sum;
        System.out.println(hilo.getName()+ " iniciando.");

        //synchronize llama a sumArray()
        synchronized (sumarray) {
            //Aquí, las llamadas a sumArray() en sumarray se sincronizan
            resp = sumarray.sumArray(a);
        }
        System.out.println("Suma para "+hilo.getName()+ " es "+resp);
        System.out.println(hilo.getName()+ " terminado.");
    }
}
class Sincronizacion {
    public static void main(String[] args) {
        int a[]={1,2,3,4,5};
        MiHilo mh1 = MiHilo.creaEInicia("#1",a);
        MiHilo mh2 = MiHilo.creaEInicia("#2",a);

        try {
            mh1.hilo.join();
            mh2.hilo.join();
        }catch (InterruptedException exc){
            System.out.println("Hilo principal interrumpido.");
        }
    }
}

Esta versión produce la misma salida correcta que la mostrada anteriormente que usa un método sincronizado (synchronized).

Hilos en Java
  • Sincronización (synchronization)

Sobre el Autor:

Hey hola! Yo soy Alex Walton y tengo el placer de compartir conocimientos hacía ti sobre el tema de Programación en Java, desde cero, Online y Gratis.

Deja una Respuesta

*

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.