Programación concurrente de Java: sincronizado y su principio de realización

1. Uso básico de Synchronized

Sincronizado es uno de los métodos más utilizados para resolver problemas de concurrencia en Java, y también es el método más simple. Synchronized tiene tres funciones principales: (1) garantizar que los subprocesos sean mutuamente excluyentes para acceder al código de sincronización (2) para garantizar que la modificación de las variables compartidas se pueda ver a tiempo (3) para resolver eficazmente el problema de reordenación. Hablando sintácticamente, hay tres usos de Synchronized:


1. Modificación de métodos ordinarios
2. Modificación de métodos estáticos
3. Modificación de bloques de código

A continuación, usaré algunos programas de ejemplo para ilustrar estas tres formas de uso (por el bien de la comparación, las tres piezas de código son básicamente las mismas excepto por las diferentes formas de usar Synchronized).

1. Cuando no hay sincronización:

Fragmento de código uno:

package com.paddx.test.concurrent;

public class SynchronizedTest {
    public void method1(){
        System.out.println("Method 1 start");
        try {
            System.out.println("Method 1 execute");
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 1 end");
    }

    public void method2(){
        System.out.println("Method 2 start");
        try {
            System.out.println("Method 2 execute");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 2 end");
    }

    public static void main(String[] args) {
        final SynchronizedTest test = new SynchronizedTest();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method1();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method2();
            }
        }).start();
    }
}

El resultado de la ejecución es el siguiente: El hilo 1 y el hilo 2 entran en estado de ejecución al mismo tiempo. El hilo 2 se ejecuta más rápido que el hilo 1, por lo que el hilo 2 se ejecuta primero. El hilo 1 y el hilo 2 se ejecutan simultáneamente en este proceso.

Method 1 start
Method 1 execute
Method 2 start
Method 2 execute
Method 2 end
Method 1 end

2. Sincronice con métodos comunes:

Fragmento de código dos:

package com.paddx.test.concurrent;

public class SynchronizedTest {
    public synchronized void method1(){
        System.out.println("Method 1 start");
        try {
            System.out.println("Method 1 execute");
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 1 end");
    }

    public synchronized void method2(){
        System.out.println("Method 2 start");
        try {
            System.out.println("Method 2 execute");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 2 end");
    }

    public static void main(String[] args) {
        final SynchronizedTest test = new SynchronizedTest();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method1();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method2();
            }
        }).start();
    }
}

El resultado de la ejecución es el siguiente: En comparación con el segmento de código, es obvio que el hilo 2 necesita esperar a que finalice la ejecución del método 1 del hilo 1 antes de comenzar a ejecutar el método 2.

Method 1 start
Method 1 execute
Method 1 end
Method 2 start
Method 2 execute
Method 2 end

3. Sincronización de métodos estáticos (clases)

Fragmento de código tres:

package com.paddx.test.concurrent;
 
 public class SynchronizedTest {
     public static synchronized void method1(){
         System.out.println("Method 1 start");
         try {
             System.out.println("Method 1 execute");
             Thread.sleep(3000);
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
         System.out.println("Method 1 end");
     }
 
     public static synchronized void method2(){
         System.out.println("Method 2 start");
         try {
             System.out.println("Method 2 execute");
             Thread.sleep(1000);
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
         System.out.println("Method 2 end");
     }
 
     public static void main(String[] args) {
         final SynchronizedTest test = new SynchronizedTest();
         final SynchronizedTest test2 = new SynchronizedTest();
 
         new Thread(new Runnable() {
             @Override
             public void run() {
                 test.method1();
             }
         }).start();
 
         new Thread(new Runnable() {
             @Override
             public void run() {
                 test2.method2();
             }
         }).start();
     }
 }

Los resultados de la ejecución son los siguientes. La sincronización de métodos estáticos es esencialmente la sincronización de clases (los métodos estáticos son esencialmente métodos que pertenecen a clases, no métodos en objetos), por lo que incluso si test y test2 pertenecen a objetos diferentes, ambos pertenecen a SynchronizedTest Instancias de clases, por lo que método1 y método2 solo se pueden ejecutar secuencialmente, no simultáneamente.

Method 1 start
Method 1 execute
Method 1 end
Method 2 start
Method 2 execute
Method 2 end

4. Sincronización de bloques de código

Fragmento de código cuatro:

package com.paddx.test.concurrent;

public class SynchronizedTest {
    public void method1(){
        System.out.println("Method 1 start");
        try {
            synchronized (this) {
                System.out.println("Method 1 execute");
                Thread.sleep(3000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 1 end");
    }

    public void method2(){
        System.out.println("Method 2 start");
        try {
            synchronized (this) {
                System.out.println("Method 2 execute");
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 2 end");
    }

    public static void main(String[] args) {
        final SynchronizedTest test = new SynchronizedTest();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method1();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method2();
            }
        }).start();
    }
}

El resultado de la ejecución es el siguiente: aunque tanto el hilo 1 como el hilo 2 entran en el método correspondiente para iniciar la ejecución, el hilo 2 necesita esperar a que se complete la ejecución del bloque de sincronización en el hilo 1 antes de entrar en el bloque de sincronización.

Method 1 start
Method 1 execute
Method 2 start
Method 1 end
Method 2 execute
Method 2 end

2. Principio sincronizado

Si tiene alguna pregunta sobre los resultados de ejecución anteriores, no se preocupe, primero comprendamos el principio de Sincronizado y luego veremos los problemas anteriores de un vistazo. Veamos primero cómo Synchronized sincroniza los bloques de código al descompilar el siguiente código:

package com.paddx.test.concurrent;

public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            System.out.println("Method 1 start");
        }
    }
}

Resultado de la descompilación:

Con respecto al papel de estas dos instrucciones, nos referimos directamente a la descripción en la especificación de JVM:

monitorenter:

Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:

• If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.

• If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.

• If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership.

El significado general de este pasaje es:

Cada objeto tiene un bloqueo de monitor (monitor). Cuando el monitor está ocupado, se bloqueará.Cuando el hilo ejecuta la instrucción de monitorizador, intenta adquirir la propiedad del monitor.El proceso es el siguiente:

Si el número de entradas del monitor es 0, el hilo ingresa al monitor y luego el número de entradas se establece en 1, y el hilo es el propietario del monitor.

Si el hilo ya ha ocupado el monitor y acaba de volver a entrar, el número de entradas que entran en el monitor aumenta en 1.

Si otros subprocesos ya han ocupado el monitor, el subproceso entra en el estado de bloqueo hasta que el número de entradas en el monitor sea 0 y luego vuelva a intentar adquirir la propiedad del monitor.

monitoreadoxit: 

El hilo que ejecuta monitorexit debe ser el propietario del monitor asociado con la instancia a la que hace referencia objectref.

El hilo disminuye el recuento de entradas del monitor asociado con objectref. Si, como resultado, el valor del recuento de entradas es cero, el hilo sale del monitor y ya no es su propietario. Otros subprocesos que bloquean la entrada al monitor pueden intentar hacerlo.

El significado general de este pasaje es:

El hilo que ejecuta monitorexit debe ser el propietario del monitor correspondiente a objectref.

Cuando se ejecuta la instrucción, el número de entrada del monitor se reduce en 1. Si el número de entrada es 0 después de restar 1, el hilo sale del monitor y ya no es el propietario del monitor. Otros subprocesos bloqueados por este monitor pueden intentar adquirir la propiedad de este monitor. 

A través de estos dos párrafos de descripción, deberíamos poder ver claramente el principio de implementación de Synchronized. La parte inferior semántica de Synchronized se completa con un objeto monitor. De hecho, esperar / notificar y otros métodos también dependen del objeto monitor. Es por eso que solo en sincronización Espere / notifique y otros métodos se pueden llamar en el bloque o método, de lo contrario arrojará java.lang.IllegalMonitorStateException.

Veamos el resultado de la descompilación del método de sincronización:

Código fuente:

package com.paddx.test.concurrent;

public class SynchronizedMethod {
    public synchronized void method() {
        System.out.println("Hello World!");
    }
}

Resultado de la descompilación:

A partir de los resultados de la descompilación, la sincronización del método no se completa con las instrucciones monitorenter y monitorexit (teóricamente, también se puede lograr con estas dos instrucciones), pero en comparación con el método ordinario, el pool constante tiene un identificador ACC_SYNCHRONIZED adicional . La JVM implementa la sincronización del método basándose en este identificador: cuando se llama al método, la instrucción de llamada verificará si el indicador de acceso ACC_SYNCHRONIZED del método está establecido. Si está establecido, el hilo de ejecución primero adquirirá el monitor y el cuerpo del método se puede ejecutar después de que la adquisición sea exitosa. , Y suelte el monitor después de que se ejecute el método. Durante la ejecución del método, ningún otro hilo puede obtener el mismo objeto monitor. De hecho, no hay diferencia en esencia, pero la sincronización de métodos se realiza de forma implícita, sin bytecode.

Tres, interpretación del resultado de la operación

Con una comprensión del principio de Sincronizado, se puede resolver mirando el programa anterior nuevamente.

1. Resultados del segmento de código 2:

Aunque el método1 y el método2 son métodos diferentes, estos dos métodos se sincronizan y se llaman a través del mismo objeto, por lo que antes de llamar, debe competir por el bloqueo (monitor) del mismo objeto. Los bloqueos se pueden adquirir de forma mutuamente excluyente, por lo tanto, el método1 y el método2 solo se pueden ejecutar de forma secuencial.

2. Resultados del segmento de código 3:

Aunque test y test2 pertenecen a objetos diferentes, test y test2 pertenecen a instancias diferentes de la misma clase. Debido a que el método1 y el método2 son métodos de sincronización estática, debes colocar el monitor en la misma clase al llamar (cada clase corresponde a un solo objeto de clase ), por lo que solo se puede ejecutar de forma secuencial.

3. Resultados del segmento de código 4:

La sincronización del bloque de código esencialmente necesita obtener el monitor del objeto entre paréntesis después de la palabra clave Synchronized. Dado que el contenido de los paréntesis en este código es este, y el método1 y el método2 se llaman a través del mismo objeto, así que antes de ingresar al bloque de sincronización Necesita competir por bloqueos en el mismo objeto, por lo que solo los bloques sincronizados se pueden ejecutar secuencialmente.

Cuatro, resumen

Sincronizado es la forma más utilizada para garantizar la seguridad de los subprocesos en la programación concurrente de Java, y su uso es relativamente simple. Pero si podemos comprender sus principios en profundidad y comprender el conocimiento subyacente de los bloqueos de monitor, por un lado, puede ayudarnos a usar la palabra clave sincronizada correctamente y, por otro lado, también puede ayudarnos a comprender mejor el mecanismo de programación concurrente y ayudarnos a Elija una estrategia de concurrencia más óptima para completar la tarea en diferentes situaciones. También es capaz de lidiar con calma con varios problemas de concurrencia encontrados en tiempos normales.

Supongo que te gusta

Origin blog.csdn.net/bj_chengrong/article/details/96830136
Recomendado
Clasificación