Palabra clave sincronizada en Java

    Introducci贸n

    Este es el segundo art铆culo de la serie de art铆culos sobre Concurrencia en Java. En el art铆culo anterior, aprendimos sobre la Executor piscina y varias categor铆as de Executors en Java.

    En este art铆culo, aprenderemos cu谩les son las synchronized es la palabra clave y c贸mo podemos usarla en un entorno de subprocesos m煤ltiples.

    驴Qu茅 es la sincronizaci贸n?

    En un entorno de subprocesos m煤ltiples, es posible que m谩s de un subproceso intente acceder al mismo recurso. Por ejemplo, dos subprocesos que intentan escribir en el mismo archivo de texto. En ausencia de sincronizaci贸n entre ellos, es posible que los datos escritos en el archivo se corrompan cuando dos o m谩s subprocesos tienen acceso de escritura al mismo archivo.

    Adem谩s, en la JVM, cada hilo almacena una copia local de variables en su pila. El valor real de estas variables puede ser cambiado por alg煤n otro hilo. Pero es posible que ese valor no se actualice en la copia local de otro hilo. Esto puede provocar una ejecuci贸n incorrecta de programas y un comportamiento no determinista.

    Para evitar estos problemas, Java nos proporciona la synchronized palabra clave, que act煤a como un bloqueo para un recurso en particular. Esto ayuda a lograr la comunicaci贸n entre subprocesos de modo que solo un subproceso acceda al recurso sincronizado y otros subprocesos esperan a que el recurso se libere.

    los synchronized La palabra clave se puede utilizar de diferentes formas, como un bloque sincronizado:

    synchronized (someObject) {
        // Thread-safe code here
    }
    

    Tambi茅n se puede utilizar con un m茅todo como este:

    public synchronized void somemMethod() {
        // Thread-safe code here
    }
    

    C贸mo funciona la sincronizaci贸n en la JVM

    Cuando un hilo intenta entrar en el bloque o m茅todo sincronizado, tiene que adquirir un bloquear en el objeto que se sincroniza. Uno y solo un hilo puede adquirir ese bloqueo a la vez y ejecutar c贸digo en ese bloque.

    Si otro subproceso intenta acceder a un bloque sincronizado antes de que el subproceso actual complete su ejecuci贸n del bloque, tiene que esperar. Cuando el hilo actual sale del bloque, el bloqueo se libera autom谩ticamente y cualquier hilo en espera puede adquirir ese bloqueo e ingresar al bloque sincronizado:

    • Para synchronized bloque, el bloqueo se adquiere en el objeto especificado entre par茅ntesis despu茅s de la synchronized palabra clave
    • Para synchronized static m茅todo, el bloqueo se adquiere en el .class objeto
    • Para synchronized m茅todo de instancia, el bloqueo se adquiere en la instancia actual de esa clase, es decir this ejemplo

    M茅todos sincronizados

    Definiendo synchronized m茅todos es tan f谩cil como simplemente incluir la palabra clave antes del tipo de retorno. Definamos un m茅todo que imprima los n煤meros entre 1 y 5 de manera secuencial.

    Dos subprocesos intentar谩n acceder a este m茅todo, as铆 que primero veamos c贸mo terminar谩 esto sin sincronizarlos, y luego bloquearemos el objeto compartido y veremos qu茅 sucede:

    public class NonSynchronizedMethod {
    
        public void printNumbers() {
            System.out.println("Starting to print Numbers for " + Thread.currentThread().getName());
    
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + " " + i);
            }
    
            System.out.println("Completed printing Numbers for " + Thread.currentThread().getName());
        }
    }
    

    Ahora, implementemos dos subprocesos personalizados que acceden a este objeto y desean ejecutar el printNumbers() m茅todo:

    class ThreadOne extends Thread {
    
        NonSynchronizedMethod nonSynchronizedMethod;
    
        public ThreadOne(NonSynchronizedMethod nonSynchronizedMethod) {
            this.nonSynchronizedMethod = nonSynchronizedMethod;
        }
    
        @Override
        public void run() {
            nonSynchronizedMethod.printNumbers();
        }
    }
    
    class ThreadTwo extends Thread {
    
        NonSynchronizedMethod nonSynchronizedMethod;
    
        public ThreadTwo(NonSynchronizedMethod nonSynchronizedMethod) {
            this.nonSynchronizedMethod = nonSynchronizedMethod;
        }
    
        @Override
        public void run() {
            nonSynchronizedMethod.printNumbers();
        }
    }
    

    Estos hilos comparten un objeto com煤n NonSynchronizedMethod y simult谩neamente intentar谩n llamar al m茅todo no sincronizado printNumbers() en este objeto.

    Para probar este comportamiento, escribamos una clase principal:

    public class TestSynchronization {
        public static void main(String[] args) {
    
            NonSynchronizedMethod nonSynchronizedMethod = new NonSynchronizedMethod();
    
            ThreadOne threadOne = new ThreadOne(nonSynchronizedMethod);
            threadOne.setName("ThreadOne");
    
            ThreadTwo threadTwo = new ThreadTwo(nonSynchronizedMethod);
            threadTwo.setName("ThreadTwo");
    
            threadOne.start();
            threadTwo.start();
    
        }
    }
    

    Ejecutar el c贸digo nos dar谩 algo como:

    Starting to print Numbers for ThreadOne
    Starting to print Numbers for ThreadTwo
    ThreadTwo 0
    ThreadTwo 1
    ThreadTwo 2
    ThreadTwo 3
    ThreadTwo 4
    Completed printing Numbers for ThreadTwo
    ThreadOne 0
    ThreadOne 1
    ThreadOne 2
    ThreadOne 3
    ThreadOne 4
    Completed printing Numbers for ThreadOne
    

    ThreadOne Empez贸 primero, aunque ThreadTwo completado primero.

    Y ejecutarlo de nuevo nos saluda con otra salida no deseada:

    Starting to print Numbers for ThreadOne
    Starting to print Numbers for ThreadTwo
    ThreadOne 0
    ThreadTwo 0
    ThreadOne 1
    ThreadTwo 1
    ThreadOne 2
    ThreadTwo 2
    ThreadOne 3
    ThreadOne 4
    ThreadTwo 3
    Completed printing Numbers for ThreadOne
    ThreadTwo 4
    Completed printing Numbers for ThreadTwo
    

    Estas salidas se dan completamente al azar y son completamente impredecibles. Cada ejecuci贸n nos dar谩 un resultado diferente. Considere esto con el hecho de que puede haber muchos m谩s hilos, y podr铆amos tener un problema. En escenarios del mundo real, es especialmente importante considerar esto al acceder a alg煤n tipo de recurso compartido, como un archivo u otro tipo de E / S, en lugar de simplemente imprimir en la consola.

    Ahora, adecuadamente synchronize nuestro m茅todo:

    public synchronized void printNumbers() {
        System.out.println("Starting to print Numbers for " + Thread.currentThread().getName());
    
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    
        System.out.println("Completed printing Numbers for " + Thread.currentThread().getName());
    }
    

    Absolutamente nada ha cambiado, adem谩s de incluir el synchronized palabra clave. Ahora, cuando ejecutamos el c贸digo:

    Starting to print Numbers for ThreadOne
    ThreadOne 0
    ThreadOne 1
    ThreadOne 2
    ThreadOne 3
    ThreadOne 4
    Completed printing Numbers for ThreadOne
    Starting to print Numbers for ThreadTwo
    ThreadTwo 0
    ThreadTwo 1
    ThreadTwo 2
    ThreadTwo 3
    ThreadTwo 4
    Completed printing Numbers for ThreadTwo
    

    Esto parece correcto.

    Aqu铆, vemos que aunque los dos subprocesos se ejecutan simult谩neamente, solo uno de los subprocesos ingresa al m茅todo sincronizado a la vez, que en este caso es ThreadOne.

    Una vez que completa la ejecuci贸n, ThreadTwo puede comienza con la ejecuci贸n del printNumbers() m茅todo.

    Bloques sincronizados

    El objetivo principal del multi-threading es ejecutar tantas tareas en paralelo como sea posible. Sin embargo, la sincronizaci贸n acelera el paralelismo de los subprocesos que deben ejecutar un m茅todo o bloque sincronizado.

    Esto reduce el rendimiento y la capacidad de ejecuci贸n paralela de la aplicaci贸n. Esta desventaja no se puede evitar por completo debido a los recursos compartidos.

    Sin embargo, podemos intentar reducir la cantidad de c贸digo que se ejecutar谩 de forma sincronizada manteniendo la menor cantidad de c贸digo posible en el alcance de synchronized. Puede haber muchos escenarios en los que, en lugar de sincronizar todo el m茅todo, est谩 bien sincronizar algunas l铆neas de c贸digo en el m茅todo.

    Podemos usar el synchronized block para incluir solo esa parte del c贸digo en lugar de todo el m茅todo.

    Dado que hay menos cantidad de c贸digo para ejecutar dentro del bloque sincronizado, cada uno de los subprocesos libera el bloqueo m谩s r谩pidamente. Como resultado, los otros subprocesos pasan menos tiempo esperando el bloqueo y el rendimiento del c贸digo aumenta considerablemente.

    Modifiquemos el ejemplo anterior para sincronizar solo el for bucle imprimiendo la secuencia de n煤meros, ya que de manera realista, es la 煤nica parte del c贸digo que debe sincronizarse en nuestro ejemplo:

    public class SynchronizedBlockExample {
    
        public void printNumbers() {
    
            System.out.println("Starting to print Numbers for " + Thread.currentThread().getName());
    
            synchronized (this) {
                for (int i = 0; i < 5; i++) {
                    System.out.println(Thread.currentThread().getName() + " " + i);
                }
            }
    
            System.out.println("Completed printing Numbers for " + Thread.currentThread().getName());
        }
    }
    

    Veamos la salida ahora:

    Starting to print Numbers for ThreadOne
    Starting to print Numbers for ThreadTwo
    ThreadOne 0
    ThreadOne 1
    ThreadOne 2
    ThreadOne 3
    ThreadOne 4
    Completed printing Numbers for ThreadOne
    ThreadTwo 0
    ThreadTwo 1
    ThreadTwo 2
    ThreadTwo 3
    ThreadTwo 4
    Completed printing Numbers for ThreadTwo
    

    Aunque parezca alarmante que ThreadTwo ha “comenzado” a imprimir n煤meros antes ThreadOne complet贸 su tarea, esto es solo porque permitimos que el hilo pasara System.out.println(Starting to print Numbers for ThreadTwo) declaraci贸n antes de parar ThreadTwo con la cerradura.

    Eso est谩 bien porque solo quer铆amos sincronizar la secuencia de los n煤meros en cada hilo. Podemos ver claramente que los dos hilos est谩n imprimiendo n煤meros en la secuencia correcta simplemente sincronizando el for lazo.

    Conclusi贸n

    En este ejemplo, vimos c贸mo podemos usar palabras clave sincronizadas en Java para lograr la sincronizaci贸n entre m煤ltiples subprocesos. Tambi茅n aprendimos cu谩ndo podemos usar m茅todos sincronizados y bloques con ejemplos.

    Como siempre, puede encontrar el c贸digo utilizado en este ejemplo Aqu铆.

     

    Etiquetas:

    Deja una respuesta

    Tu direcci贸n de correo electr贸nico no ser谩 publicada. Los campos obligatorios est谩n marcados con *