¿Cuál es el papel de 62 volátiles? ¿Cuáles son las similitudes y diferencias con sincronizado?

Que es volátil

Primero, introduzcamos volatile, que es una palabra clave en Java y un mecanismo de sincronización. Cuando una variable es una variable compartida, y la variable es modificada por volátil, luego de modificar el valor de la variable y luego leer el valor de la variable, puede asegurarse de que se obtiene el último valor después de la modificación, y es no es un valor vencido.

En comparación con el sincronizado o el bloqueo, el volátil es más liviano, porque el uso de volátiles no causará gastos generales elevados, como el cambio de contexto, y no bloqueará los subprocesos. Pero precisamente debido a su sobrecarga relativamente pequeña, su efecto, es decir, su capacidad, es relativamente pequeño.

Aunque el volátil se utiliza para garantizar la seguridad de los subprocesos, no puede lograr una protección de sincronización como el sincronizado. El volátil solo puede desempeñar un papel en escenarios muy limitados, así que echemos un vistazo a sus escenarios aplicables. Primero, veremos los escenarios que no son adecuados para usar se dan los volátiles y luego se dan dos escenarios para el uso de volátiles.

Las ocasiones aplicables de volátiles

No aplicable: a ++ En
primer lugar, echemos un vistazo a los escenarios en los que lo volátil no es adecuado. Lo volátil no es adecuado para escenarios en los que se debe garantizar la atomicidad. Por ejemplo, se debe confiar en el valor original al actualizar. El más típico El escenario es un ++. Solo confiamos en volátiles no podemos garantizar la seguridad de subprocesos de un ++. El código es el siguiente:

public class DontVolatile implements Runnable {
    
    
    volatile int a;
    AtomicInteger realA = new AtomicInteger();
    public static void main(String[] args) throws InterruptedException {
    
    
        Runnable r =  new DontVolatile();
        Thread thread1 = new Thread(r);
        Thread thread2 = new Thread(r);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(((DontVolatile) r).a);
        System.out.println(((DontVolatile) r).realA.get());
    }
    @Override
    public void run() {
    
    
        for (int i = 0; i < 1000; i++) {
    
    
            a++;
            realA.incrementAndGet();
        }
    }
}

En este código, tenemos una variable int tipo a modificada volátil, y también hay una clase atómica realA a continuación. La clase atómica puede garantizar la seguridad de los subprocesos, por lo que la usamos para comparar con int a volátil. Eche un vistazo a la diferencia en sus efectos reales.

En la función principal, creamos dos nuevos hilos y los dejamos correr. El contenido de estos dos subprocesos en ejecución es realizar 1000 operaciones de acumulación. Cada operación de acumulación realizará una operación de auto-suma en la variable volátil modificada a, y al mismo tiempo realizará una operación de auto-suma en la clase atómica realA. Cuando los dos subprocesos han terminado de ejecutarse, imprimimos los resultados. Uno de los resultados es el siguiente:

1988
2000

Encontrará que el valor final a y el valor realA son 1988 y 2000, respectivamente. Se puede ver que incluso si la variable a es modificada por volátil, incluso si finalmente realiza un total de 2000 operaciones de auto-adición (esto puede ser confirmado por el valor final de la clase atómica), pero algunas operaciones de auto-adición son sigue siendo inválido, por lo que al final Su resultado es inferior a 2000, lo que demuestra que lo volátil no puede garantizar la atomicidad. Entonces, ¿para qué escenarios es adecuado?

Ocasión aplicable 1: bit de bandera booleana

Si una variable compartida solo es asignada o leída por cada hilo de principio a fin, y no hay otra operación (como leer y modificar sobre esta base, una operación compuesta), entonces podemos usar volátil en lugar de sincronizada o atómica. class, porque la operación de asignación en sí es atómica, volátil al mismo tiempo garantiza la visibilidad, que es suficiente para garantizar la seguridad de los subprocesos.

Un escenario típico es el escenario de bits de bandera booleana, como una bandera booleana volátil. Porque en circunstancias normales, el bit de bandera del tipo booleano se asignará directamente. En este momento, no habrá operación compuesta (como a ++). Solo hay una operación única, que es cambiar el valor de la bandera. Una vez que la bandera es modificada por volátil, se puede garantizar la visibilidad, luego esta bandera se puede usar como un bit de bandera. En este momento, una vez que su valor cambia, todos los hilos pueden verlo inmediatamente, por lo que es muy adecuado usar volatile. aquí.

Echemos un vistazo al ejemplo de código.

public class YesVolatile1 implements Runnable {
    
    
    volatile boolean done = false;
    AtomicInteger realA = new AtomicInteger();
    public static void main(String[] args) throws InterruptedException {
    
    
        Runnable r =  new YesVolatile1();
        Thread thread1 = new Thread(r);
        Thread thread2 = new Thread(r);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(((YesVolatile1) r).done);
        System.out.println(((YesVolatile1) r).realA.get());
    }
    @Override
    public void run() {
    
    
        for (int i = 0; i < 1000; i++) {
    
    
            setDone();
            realA.incrementAndGet();
        }
    }
    private void setDone() {
    
    
        done = true;
    }
}

Este código es muy similar al código anterior, la única diferencia es que cambiamos volatile int a a volatile boolean done, y el método setDone () fue llamado durante la operación de 1000 ciclos, y este setDone () El método es establecer la variable done a verdadera en lugar de emitir juicios basados ​​en su valor original. Por ejemplo, si resulta ser falso, establézcalo en verdadero, o si resulta ser verdadero, configúrelo en falso. Estos juicios complejos no son available, setDone El método () establece directamente el valor de la variable done en verdadero. Entonces, el resultado final de este código es el siguiente:

true
2000

No importa cuántas veces se ejecute, la consola imprimirá verdadero y 2000. El 2000 impreso ha confirmado que se han realizado 2000 operaciones, y el resultado verdadero final demuestra que, en este escenario, lo volátil juega un papel en garantizar el efecto de seguridad de subprocesos.

La mayor diferencia entre el segundo ejemplo y el primer ejemplo es que la operación del primer ejemplo es a ++, que es una operación compuesta y no tiene atomicidad, y la operación en este ejemplo es solo para establecer done en verdadero, tal operación de asignación en sí mismo es atómico, por lo que en este ejemplo, es adecuado usar volátil.

Ocasión aplicable 2: como disparador

Entonces, veamos el segundo escenario donde lo volátil es adecuado: como un disparador para asegurar la visibilidad de otras variables.

Aquí hay un ejemplo clásico proporcionado por Brian Goetz:

Map configOptions;
char[] configText;
volatile boolean initialized = false;
 
. . .
 
// In thread A
 
configOptions = new HashMap();
configText = readConfigFile(fileName);
processConfigOptions(configText, configOptions);
initialized = true;
 
. . .
 
// In thread B
 
while (!initialized) 
  sleep();
// use configOptions

Como puede ver en este código, tenemos un mapa llamado configOptions y una matriz de caracteres llamada configText, y luego habrá un booleano inicializado modificado por volatile, que inicialmente es igual a falso. Las siguientes cuatro líneas de código son ejecutadas por el hilo A, lo que hace es inicializar configOptions, luego inicializar configText y luego poner estos dos valores en un método para ejecutar, de hecho, todos estos representan el comportamiento de inicialización. Luego, una vez que se ejecutan estos métodos, significa que el trabajo de inicialización se completa y el hilo A establecerá la variable inicializada en verdadera.

Para el subproceso B, ejecutará repetidamente el método de suspensión en el ciclo while al principio (por ejemplo, suspensión durante un período de tiempo), hasta que la variable inicializada se convierta en verdadera, el subproceso B omitirá el método de suspensión y continuará la ejecución. El punto importante es que una vez inicializado se convierte en verdadero, el subproceso B usará inmediatamente estas configOptions, por lo que esto requiere que las configOptions en este momento se inicialicen, y el resultado de la operación de inicialización debe ser para el subproceso B Se puede ver que, de lo contrario, el subproceso B puede informar de un error durante la ejecución.

Es posible que esté preocupado, debido a que este configOptions se modifica en el hilo A, ¿ocurrirá el problema de visibilidad cuando se lea en el hilo B y no leerá el valor después de la inicialización? Si no usamos volatile, entonces este problema existe.

Pero ahora usamos inicializado modificado por volátil como disparador, por lo que este problema está resuelto. De acuerdo con la regla de un solo subproceso de la relación pasa antes, la inicialización de configOptions en el subproceso A sucede antes escribe en la variable inicializada, y la lectura de initialzed en el subproceso B usa la variable configOptions de acuerdo con la relación pasa antes. De acuerdo con la regla volátil del subproceso A, la operación que escribe inicializada en verdadero en el subproceso A ocurre antes de la lectura posterior de las variables inicializadas en el subproceso B.

Si tenemos la operación A y la operación B respectivamente, usamos hb (A, B) para representar que A ocurre antes de B. Sucede antes es transitivo Si hb (A, B) y hb (B, C), entonces hb (A, C) puede derivarse. Entonces, en base a las condiciones anteriores, podemos sacar una conclusión: la inicialización de configOptions en el hilo A ocurre antes del uso de configOptions en el hilo B. Entonces, para el subproceso B, dado que ha visto el último valor inicializado, también puede ver el estado inicializado de estas variables, incluidas las configOptions, por lo que es seguro para el subproceso B usar configOptions en este momento. Este uso es utilizar la variable modificada por volatile como disparador para asegurar la visibilidad de otras variables Este uso también es muy digno de ser captado y puede usarse como un punto culminante durante la entrevista.

El papel de los volátiles

Hemos analizado dos usos muy típicos arriba, así que resumamos el papel de volátil, que tiene dos funciones.

El papel de la primera capa es garantizar la visibilidad. La relación Sucede-antes describe lo volátil de la siguiente manera: una operación de escritura en una variable volátil ocurre antes de una operación de lectura posterior en esa variable.

Esto significa que si una variable es modificada por volátil, luego de cada modificación, se debe leer el último valor de la variable cuando la variable se lea a continuación.

El papel de la segunda capa es prohibir el reordenamiento. Permítanme presentarles la semántica de as-if-serial primero: no importa cómo lo reordene, el resultado de la ejecución del programa (de un solo subproceso) no cambiará. Bajo la premisa de satisfacer la semántica como-si-serial, debido a la optimización del compilador o CPU, el orden de ejecución real del código puede ser diferente al orden que escribimos. Esto no es un problema en el caso de un solo hilo, pero una vez que se introduce el subproceso múltiple, este tipo de desorden puede causar serios problemas de seguridad en los subprocesos. Este reordenamiento puede prohibirse hasta cierto punto mediante el uso de la palabra clave volatile.

La relación entre volátil y sincronizado

Echemos un vistazo a la relación entre volátil y sincronizado:

Similitud: Volátil se puede considerar como una versión ligera de sincronizada. Por ejemplo, si una variable compartida solo es asignada y leída por cada hilo de principio a fin, y no hay otra operación, entonces volátil se puede usar en lugar de sincronizada o en su lugar de variables atómicas, que es suficiente Garantizar la seguridad de los subprocesos. De hecho, cada lectura o escritura en un campo volátil es similar a "semisincrónica": la lectura de volátiles tiene la misma semántica de memoria que la adquisición de un bloqueo sincronizado y la escritura de volátiles tiene la misma semántica que la liberación de un bloqueo sincronizado.

Irreemplazable: Pero en más casos, volátil no puede reemplazar sincronizado y volátil no proporciona atomicidad y exclusión mutua.

Rendimiento: Las operaciones de lectura y escritura de atributos volátiles están libres de bloqueos. Como no hay bloqueo, no es necesario dedicar tiempo a adquirir y liberar bloqueos. Por lo tanto, es de alto rendimiento y tiene un mejor rendimiento que el sincronizado.

resumen

Las dos funciones de olatile, la primera es garantizar la visibilidad y la segunda es prohibir el reordenamiento.

Supongo que te gusta

Origin blog.csdn.net/Rinvay_Cui/article/details/111058986
Recomendado
Clasificación