5 casos y diagramas de flujo lo ayudan a comprender la palabra clave volátil de 0 a 1

volátil

Con la mejora del hardware, el número de núcleos de la máquina ha cambiado de un solo núcleo a varios núcleos. Para mejorar la utilización de la máquina, la programación concurrente se ha vuelto cada vez más importante y se ha convertido en una máxima prioridad en el trabajo y en entrevistas Para comprender y utilizar mejor la programación concurrente, debe crear su propio sistema de conocimiento de programación concurrente Java.

Este artículo se centrará en la palabra clave volátil en Java y describirá la atomicidad, la visibilidad, el orden, el papel de los volátiles, los principios de implementación, los escenarios de uso y problemas relacionados, como JMM y pseudo-compartir, de una manera simple y fácil de entender.

Para describir mejor lo volátil, primero hablemos de su conocimiento previo: orden, visibilidad y atomicidad.

Orden

¿Qué es el orden?

Cuando utilizamos lenguajes de alto nivel y sintaxis simple para programar, en última instancia tenemos que traducir el lenguaje en instrucciones que la CPU comprenda.

Dado que es la CPU la que hace el trabajo, para acelerar la utilización de la CPU, se reordenarán las instrucciones para nuestro control de procesos.

En el modelo de memoria Java, las reglas de reordenamiento de instrucciones deben satisfacer la regla de sucede antes. Por ejemplo, el inicio de un subproceso debe ocurrir antes que otras operaciones del subproceso. Las instrucciones que inician el subproceso no se pueden reordenar en el subproceso. las tareas realizadas

Es decir, en el modelo de memoria Java, el reordenamiento de instrucciones no afectará el proceso de ejecución de un solo subproceso que hemos especificado , pero en el caso de subprocesos múltiples, no se puede estimar el proceso de ejecución de cada subproceso.

Para obtener una descripción más apropiada, consulte el siguiente fragmento de código.

    static int a, b, x, y;

    public static void main(String[] args){
        long count = 0;
        while (true) {
            count++;
            a = 0;b = 0;x = 0;y = 0;
            Thread thread1 = new Thread(() -> {
                a = 1;
                x = b;
            });
            Thread thread2 = new Thread(() -> {
                b = 1;
                y = a;
            });
            thread1.start();
            thread2.start();

            try {
                thread1.join();
                thread2.join();
            } catch (Exception e) {}

            if (x == 0 && y == 0) {
                break;
            }
        }
        //count=118960,x=0,y=0
        System.out.println("count=" + count + ",x=" + x + ",y=" + y);
    }

Inicialice las cuatro variables a, b, x e y a 0

Según nuestro pensamiento, la orden de ejecución es

//线程1
a = 1;
x = b;

//线程2
b = 1;
y = a;

Sin embargo, después de reordenar las instrucciones, pueden ocurrir cuatro situaciones:

//线程1
//1           2           3           4     
a = 1;      a = 1;      x = b;      x = b;        
x = b;      x = b;      a = 1;      a = 1;  

//线程2
//1           2           3           4 
b = 1;      y = a;      b = 1;      y = a;
y = a;      b = 1;      y = a;      b = 1;

Cuando ocurre la cuarta situación, tanto x como y pueden ser 0

Entonces, ¿cómo podemos garantizar el orden?

Utilice volátil para modificar variables y garantizar el orden.

Para mejorar la utilización de la CPU, las instrucciones se reordenarán, y el reordenamiento solo puede garantizar la lógica de ejecución del proceso bajo un solo subproceso.

El orden de ejecución no se puede predecir en subprocesos múltiples y no se puede garantizar el orden. Si desea garantizar el orden en subprocesos múltiples, puede usar volátil. Volatile utilizará barreras de memoria para prohibir el reordenamiento de instrucciones para lograr el orden.

También puede bloquear directamente para garantizar la ejecución sincrónica.

Al mismo tiempo, puede utilizar Unsafela barrera de memoria de la clase en el paquete concurrente para prohibir el reordenamiento.

//线程1
a = 1;
unsafe.fullFence();
x = b;

//线程2
b = 1;   
unsafe.fullFence();
y = a;

visibilidad

¿Qué es la visibilidad?

En el modelo de memoria Java, cada hilo tiene su propia memoria de trabajo y memoria principal. Al leer datos, es necesario copiarlos de la memoria principal a la memoria de trabajo. Al modificar los datos, solo se modifican en su propia memoria de trabajo. Si varios subprocesos manipulan ciertos datos al mismo tiempo y la modificación no se vuelve a escribir en la memoria principal, otros subprocesos no podrán detectar los cambios de datos.

imagen-20230823223141451.png

Por ejemplo, en el siguiente código, el hilo creado seguirá repitiendo porque no puede detectar que otros hilos modifican variables.

    //nonVolatileNumber 是未被volatile修饰的
    new Thread(() -> {
        while (nonVolatileNumber == 0) {
    
        }
    }).start();
    
    TimeUnit.SECONDS.sleep(1);
    nonVolatileNumber = 100;

Entonces, ¿cómo se hace visible esta variable?

Puede utilizar modificaciones volátiles en esta variable para garantizar la visibilidad, o puede bloquearla sincronizada. Después del bloqueo, equivale a volver a leer los datos en la memoria principal.

atomicidad

¿Qué es la atomicidad?

De hecho, la cuestión es si una o un grupo de operaciones pueden completarse al mismo tiempo; de lo contrario, todas deben fallar, en lugar de que algunas tengan éxito y otras fracasen.

La máquina virtual implementa la atomicidad de las instrucciones de lectura y carga (arriba) en el modelo de memoria Java.

Para utilizar el incremento automático de una variable, primero es necesario leerla desde la memoria principal, luego modificarla y finalmente volver a escribirla en la memoria principal.

Entonces, ¿puede la volátil garantizar la atomicidad?

Usamos dos subprocesos para incrementar la misma variable modificada con volátil diez mil veces.

        private volatile int num = 0;
    
        public static void main(String[] args) throws InterruptedException {
            C_VolatileAndAtomic test = new C_VolatileAndAtomic();
            Thread t1 = new Thread(() -> {
                forAdd(test);
            });
    
            Thread t2 = new Thread(() -> {
                forAdd(test);
            });
    
            t1.start();
            t2.start();
    
            t1.join();
            t2.join();
    
            //13710
            System.out.println(test.num);
        }
    
        /**
         * 循环自增一万次
         *
         * @param test
         */
        private static void forAdd(C_VolatileAndAtomic test) {
            for (int i = 0; i < 10000; i++) {
                test.num++;
            }
        }

Desafortunadamente, el resultado no es 20000, lo que indica que las variables modificadas por volátiles no pueden garantizar su atomicidad.

Entonces, ¿qué método puede garantizar la atomicidad?

El método de bloqueo sincronizado puede garantizar la atomicidad, porque solo él puede acceder al bloqueo al mismo tiempo.

Usando clases atómicas, el método subyacente de usar CAS también puede garantizar la atomicidad.¿Qué es CAS? Hablaremos de ello en artículos posteriores.

principio volátil

Después de describir y probar el orden, la visibilidad y la atomicidad, podemos saber que volátil puede garantizar el orden y la visibilidad, pero no puede garantizar la atomicidad .

Entonces, ¿cómo logra la capa inferior volátil orden y visibilidad?

La JVM agregará indicadores de acceso volátil a las variables modificadas con volatile y utilizará la barrera de memoria del sistema operativo para prohibir el reordenamiento de instrucciones cuando se ejecuten instrucciones de código de bytes.

La barrera de memoria universal comúnmente utilizada es storeload, store1 storeload load2, que prohíbe que las instrucciones de escritura se reorganicen debajo de la barrera y prohíbe que las instrucciones de lectura se reorganicen por encima de la barrera, es decir, la memoria escrita por la tienda es visible (percibible). a otros procesadores. Las lecturas de carga posteriores se leerán desde la memoria.

La implementación de la instrucción de ensamblaje volátil es en realidad una instrucción de prefijo de bloqueo.

La instrucción de prefijo de bloqueo no tiene ningún impacto en un solo núcleo, porque un solo núcleo puede garantizar el orden, la visibilidad y la atomicidad.

La instrucción de prefijo de bloqueo volverá a escribir los datos en la memoria al modificarlos en multinúcleo. La escritura de nuevo en la memoria debe garantizar que solo un procesador funcione al mismo tiempo. Esto se puede hacer bloqueando el bus, pero otros procesadores no puede acceder a él.

Para mejorar la granularidad de la concurrencia, el procesador admite el bloqueo de caché (solo bloquea líneas de caché) y utiliza el protocolo de coherencia de caché para garantizar que los mismos datos de línea de caché no se puedan modificar al mismo tiempo.

Después de volver a escribir en la memoria, se utiliza tecnología de rastreo para permitir que otros procesadores detecten cambios en los datos y vuelvan a leer la memoria antes de su uso posterior.

Problema de intercambio falso

Dado que cada lectura es una operación en una línea de caché, si varios subprocesos modifican con frecuencia dos variables en la misma línea de caché, ¿hará que otros procesadores que usan la línea de caché siempre necesiten leer datos nuevamente?

Este es en realidad el llamado problema de pseudocompartir.

Por ejemplo, dos variables i1 e i2 están en la misma línea de caché. El procesador 1 escribe con frecuencia en i1 y el procesador 2 escribe con frecuencia en i2. Tanto i1 como i2 son modificados por volátil, lo que también hace que i1 se modifique. Cuando, procesador 2 detecta que la línea de caché está sucia, por lo que necesita volver a leer la memoria para obtener la última línea de caché, pero tal sobrecarga de rendimiento no tiene ningún sentido para que el procesador 2 escriba en i2.

imagen-20230824210222603.png

Una forma común de resolver el problema del intercambio falso es agregar suficientes campos entre estos dos campos para que no estén en la misma línea de caché, lo que también generará una pérdida de espacio.

Para resolver el problema del intercambio falso, JDK también proporciona @sun.misc.Contendedanotaciones para ayudarnos a completar los campos.

El siguiente código hace que dos subprocesos se repitan mil millones de veces para realizar un autoincremento. Cuando ocurre un problema de intercambio falso, toma más de 30 segundos. Cuando no ocurre un problema de intercambio falso, solo toma unos segundos.

Cabe señalar que @sun.misc.Contendedcuando se utilizan anotaciones, es necesario llevar los parámetros de JVM.-XX:-RestrictContended

        @sun.misc.Contended
        private volatile int i1 = 0;
        @sun.misc.Contended
        private volatile int i2 = 0;
        
        public static void main(String[] args) throws InterruptedException {
            D_VolatileAndFalseSharding test = new D_VolatileAndFalseSharding();
            int count = 1_000_000_000;
            Thread t1 = new Thread(() -> {
                for (int i = 0; i < count; i++) {
                    test.i1++;
                }
            });
    
            Thread t2 = new Thread(() -> {
                for (int i = 0; i < count; i++) {
                    test.i2++;
                }
            });
    
            long start = System.currentTimeMillis();
    
            t1.start();
            t2.start();
    
            t1.join();
            t2.join();
    
            //31910 i1:1000000000 i2:1000000000
    
            //使用@sun.misc.Contended解决伪共享问题  需要携带JVM参数 -XX:-RestrictContended
            //5961 i1:1000000000 i2:1000000000
            System.out.println((System.currentTimeMillis() - start) + " i1:"+ test.i1 + " i2:"+ test.i2);
        }

escenarios de uso volátiles

Volatile prohíbe el reordenamiento de instrucciones a través de barreras de memoria para garantizar la visibilidad y el orden.

Según las características de visibilidad, volátil es muy adecuado para su uso en escenarios de lectura en programación concurrente, porque volátil garantiza visibilidad y con operaciones de lectura sin bloqueo , la sobrecarga es muy pequeña.

Por ejemplo: el estado de sincronización de la cola AQS en el paquete concurrente se modificará con volátil

Las operaciones de escritura a menudo requieren garantías de sincronización de bloqueo

Según las características de orden, volátil puede prohibir el reordenamiento de las instrucciones de creación de objetos al verificar dos veces los bloqueos, evitando así que otros subprocesos adquieran objetos que aún no se han inicializado.

La creación de un objeto se puede dividir en tres pasos:

//1.分配内存
//2.初始化对象
//3.将对象指向分配的空间

Dado que los pasos 2 y 3 dependen del paso 1, el paso 1 no se puede reordenar y los pasos 2 y 3 no tienen dependencias, por lo que reordenar puede hacer que el objeto apunte primero al espacio asignado y luego lo inicialice.

Si en este momento en el bloqueo de doble detección, un hilo determina que no está vacío y usa este objeto, pero no se ha inicializado en este momento, puede haber un problema de que el objeto obtenido aún no se ha inicializado.

Por lo tanto, el bloqueo de doble verificación correcto debe agregar volátil para prohibir el reordenamiento.

        private static volatile Singleton singleton;
    
        public static Singleton getSingleton(){
            if (Objects.isNull(singleton)){
                //有可能很多线程阻塞到拿锁,拿完锁再判断一次
                synchronized (Singleton.class){
                    if (Objects.isNull(singleton)){
                        singleton = new Singleton();
                    }
                }
            }
    
            return singleton;
        }

Resumir

Este artículo se centra en la palabra clave volátil para describir el orden, la visibilidad, la atomicidad, JMM, los principios volátiles, los escenarios de uso, los problemas de pseudocompartición, etc.

Para mejorar la utilización de la CPU, las instrucciones se reordenarán. El reordenamiento no afectará el proceso de señalización en un solo subproceso, pero no se puede predecir el proceso de ejecución en múltiples subprocesos.

En el modelo de memoria Java, cada subproceso tiene su propia memoria de trabajo. La lectura de datos debe leerse desde la memoria principal y la modificación de datos debe volver a escribirse en la memoria principal; en la programación concurrente, cuando otros subprocesos no pueden sentir que el La variable ha sido modificada. Si continúa usándola, puede cometer un error.

Volatile prohíbe el reordenamiento de instrucciones a través de barreras de memoria para lograr orden y visibilidad, pero no puede satisfacer la atomicidad.

El ensamblaje subyacente de volátil se implementa mediante la instrucción de prefijo de bloqueo. Al modificar datos en multinúcleo, el bus se bloqueará para escribir los datos de nuevo en la memoria. Debido al alto costo de bloquear el bus, la línea de caché se bloqueará. Se bloquea más tarde y se utilizará el protocolo de coherencia de caché para garantizar que solo se pueda procesar un proceso al mismo tiempo. El procesador modifica la misma línea de caché y utiliza tecnología de rastreo para permitir que otros procesadores propietarios de la línea de caché perciban que el La línea de caché está sucia y luego vuelve a leerla.

Si las variables escritas con frecuencia por varios subprocesos están en la misma línea de caché, se producirán problemas de intercambio falso. En este momento, debe completar los campos para que no estén en la misma línea de caché.

Según las características de visibilidad, volátil se usa a menudo para implementar operaciones de lectura sin bloqueo en programación concurrente; según las características de orden, puede garantizar que la instancia obtenida no esté sin inicializar en bloqueos de doble detección.

Finalmente (no lo hagas gratis, solo presiona tres veces seguidas para pedir ayuda~)

Este artículo se incluye en la columna " De punto a línea y de línea a superficie" para construir un sistema de conocimiento de programación concurrente Java en términos simples . Los estudiantes interesados ​​​​pueden seguir prestando atención.

Las notas y los casos de este artículo se han incluido en gitee-StudyJava y github-StudyJava . Los estudiantes interesados ​​pueden continuar prestando atención en stat ~

Dirección del caso:

Gitee-JavaConcurrentProgramming/src/main/java/A_volatile

Github-JavaConcurrentProgramming/src/main/java/A_volatile

Si tiene alguna pregunta, puede discutirla en el área de comentarios. Si cree que la escritura de Cai Cai es buena, puede darle me gusta, seguirla y recopilarla para respaldarla ~

Lei Jun: La versión oficial del nuevo sistema operativo de Xiaomi, ThePaper OS, ha sido empaquetada. Una ventana emergente en la página de lotería de la aplicación Gome insulta a su fundador. El gobierno de Estados Unidos restringe la exportación de la GPU NVIDIA H800 a China. La interfaz de Xiaomi ThePaper OS está expuesto. Un maestro usó Scratch para frotar el simulador RISC-V y se ejecutó con éxito. Kernel de Linux Escritorio remoto RustDesk 1.2.3 lanzado, soporte mejorado para Wayland Después de desconectar el receptor USB de Logitech, el kernel de Linux falló Revisión aguda de DHH de "herramientas de empaquetado ": no es necesario construir la interfaz en absoluto (Sin compilación) JetBrains lanza Writerside para crear documentación técnica Herramientas para Node.js 21 lanzadas oficialmente
{{o.nombre}}
{{m.nombre}}

Supongo que te gusta

Origin my.oschina.net/u/6903207/blog/10112325
Recomendado
Clasificación