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 *