Después de trabajar durante 5 años, ¿ni siquiera conozco la palabra clave volátil?

"Después de trabajar durante 5 años, ¡ni siquiera conozco la palabra clave volátil!"

Después de escuchar al arquitecto que acaba de terminar la entrevista, también participaron varios compañeros más.

Se dice que las entrevistas domésticas son "entrevistar a los portaaviones y apretar los tornillos en el trabajo". A veces lo aprobarán debido a un problema.

¿Cuánto tiempo has estado trabajando? ¿Conoce la palabra clave volátil?

¡Hoy, aprendamos juntos sobre la palabra clave volátil y seamos un trabajador de tornillos que puede construir un portaaviones en una entrevista!

Introducción a volatile +

volátil

La definición de volátil en la tercera edición de la Especificación del lenguaje Java es la siguiente:

El lenguaje de programación Java permite que los subprocesos accedan a variables compartidas. Para garantizar que las variables compartidas se puedan actualizar de forma precisa y coherente, los subprocesos deben asegurarse de que esta variable se obtenga por separado a través de un bloqueo exclusivo.

El lenguaje Java proporciona volatilidad, que es más conveniente que los bloqueos en algunos casos.

Si un campo se declara volátil, el modelo de memoria de subprocesos de Java garantiza que todos los subprocesos vean el valor de esta variable de forma coherente.

Semántica

Una vez que una variable compartida (variable miembro de clase, variable miembro estática de clase) es modificada por volátil, entonces tiene dos capas de semántica:

  1. Esto asegura la visibilidad de los diferentes hilos que operan en esta variable, es decir, si un hilo modifica el valor de una variable, el nuevo valor es inmediatamente visible para otros hilos.

  2. Está prohibido reordenar instrucciones.
  • Nota

Si la variable final también se declara como volátil, entonces se trata de un error en tiempo de compilación.

ps: Uno significa que los cambios son visibles y el otro significa que nunca cambian. El fuego natural y el agua son incompatibles.

Introducción al problema

  • Error.java
//线程1
boolean stop = false;
while(!stop){
    doSomething();
}

//线程2
stop = true;

Este código es un fragmento de código típico, y muchas personas pueden usar este método de marcado al interrumpir un hilo.

análisis del problema

Pero, de hecho, ¿este código se ejecutará correctamente? ¿Se interrumpirá el hilo?

No necesariamente, quizás en la mayoría de las veces, este código puede interrumpir el hilo, pero también puede provocar que el hilo no se interrumpa (aunque esta posibilidad es muy pequeña, una vez que esto suceda, provocará un bucle infinito).

A continuación se explica por qué este código puede hacer que el hilo no se interrumpa.

Como se explicó anteriormente, cada subproceso tiene su propia memoria de trabajo durante la operación, por lo que cuando el subproceso 1 se está ejecutando, copiará el valor de la variable de detención y lo colocará en su propia memoria de trabajo.

Luego, cuando el hilo 2 cambia el valor de la variable de parada, pero no ha tenido tiempo de escribirlo en la memoria principal, el hilo 2 cambia para hacer otras cosas,

Entonces, el hilo 1 no conoce los cambios del hilo 2 en la variable de parada, por lo que continuará en bucle.

Usar volátil

Primero: el uso de la palabra clave volátil obligará a que el valor modificado se escriba en la memoria principal inmediatamente;

Segundo: usando la palabra clave volátil, cuando se modifica el hilo 2, la línea de caché de la variable de caché que se detiene en la memoria de trabajo del hilo 1 no será válida (reflejada en la capa de hardware, es la línea de caché correspondiente en el caché L1 o L2 de la CPU inválido);

Tercero: Debido a que la línea de caché de la parada de la variable del búfer en la memoria de trabajo del hilo 1 no es válida, el hilo 1 irá a la memoria principal para leer el valor de la parada de la variable nuevamente.

Luego, cuando el hilo 2 modifica el valor de parada (por supuesto, esto incluye dos operaciones, modificar el valor en la memoria de trabajo del hilo 2 y luego escribir el valor modificado en la memoria),
la línea de caché de la variable parada se almacenará en caché en la memoria de trabajo del hilo 1 Inválido, y luego cuando el hilo 1 lee,
encuentra que su línea de caché no es válida. Esperará a que se actualice la dirección de la memoria principal correspondiente a la línea de caché, y luego irá a la memoria principal correspondiente para leer el último valor.

Luego, el hilo 1 lee el último valor correcto.

¿Lo volátil garantiza la atomicidad?

Por lo anterior, sabemos que la palabra clave volatile garantiza la visibilidad de la operación, pero ¿puede volatile garantizar que la operación de la variable sea atómica?

Introducción al problema

public class VolatileAtomicTest {

    public volatile int inc = 0;

    public void increase() {
        inc++;
    }

    public static void main(String[] args) {
        final VolatileAtomicTest test = new VolatileAtomicTest();
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    test.increase();
                }
            }).start();
        }

        //保证前面的线程都执行完
        while (Thread.activeCount() > 1) {
            Thread.yield();
        }
        System.out.println(test.inc);
    }
}
  • ¿Cuál es el resultado del cálculo?

Puede pensar que es 10,000, pero en realidad es más pequeño que este número.

la razón

Tal vez algunos amigos tengan preguntas. Eso no es correcto. Lo anterior es para auto-incrementar la variable inc. Debido a que volatile garantiza visibilidad,
luego del auto-incremento de inc en cada hilo, se puede ver en otros hilos. El valor modificado, por lo que 10 subprocesos han realizado 1000 operaciones respectivamente, entonces el valor inc final debe ser 1000 * 10 = 10000.

Aquí hay un malentendido: la palabra clave volátil puede garantizar que la visibilidad no sea incorrecta, pero el programa anterior es incorrecto en el sentido de que no garantiza la atomicidad.

La visibilidad solo puede garantizar que se lea el último valor cada vez, pero volatile no puede garantizar la atomicidad de las operaciones en las variables.

  • Solución

Utilice Lock sincronizado o AtomicInteger

¿Puede garantizar el orden volátil?

La palabra clave volátil prohíbe el reordenamiento de instrucciones tiene dos significados:

  1. Cuando el programa ejecuta la operación de lectura o escritura de la variable volátil, se deben haber realizado todos los cambios en la operación anterior, y el resultado ha sido visible para la operación posterior; la operación posterior no debe haberse realizado;

  2. Al optimizar las instrucciones, no puede colocar las instrucciones que acceden a las variables volátiles detrás de ellas para su ejecución, y no puede colocar las instrucciones que siguen a las variables volátiles antes de ellas para su ejecución.

Ejemplo

  • Ejemplo uno
//x、y为非volatile变量
//flag为volatile变量

x = 2;        //语句1
y = 0;        //语句2
flag = true;  //语句3
x = 4;        //语句4
y = -1;       //语句5

Dado que la variable de bandera es una variable volátil, en el proceso de reordenación de instrucciones, la declaración 3 no se colocará antes de la declaración 1 y la declaración 2, y la declaración 3 no se colocará después de la declaración 4 y la declaración 5.

Pero tenga en cuenta que el orden del enunciado 1 y el enunciado 2, y el orden del enunciado 4 y el enunciado 5 no están garantizados.

Y la palabra clave volátil puede garantizar que cuando se ejecuta la declaración 3, se deben ejecutar las declaraciones 1 y 2, y los resultados de ejecución de las declaraciones 1 y 2 son visibles para las declaraciones 3, 4 y 5.

  • Ejemplo dos
//线程1:
context = loadContext();   //语句1
inited = true;             //语句2

//线程2:
while(!inited ){
  sleep()
}
doSomethingwithconfig(context);

En el ejemplo anterior, se mencionó que la instrucción 2 se ejecutará antes que la instrucción 1, por lo que el contexto no se ha inicializado durante mucho tiempo y el contexto no inicializado se usa en el hilo 2 para operar, lo que provoca errores en el programa.

Si la variable iniciada se modifica con la palabra clave volatile, este tipo de problema no ocurrirá, porque cuando se ejecuta la instrucción 2, se debe garantizar que el contexto se ha inicializado.

Escenarios de uso comunes

La palabra clave volátil tiene un mejor rendimiento que sincronizada en algunos casos,

Pero tenga en cuenta que la palabra clave volátil no puede reemplazar la palabra clave sincronizada, porque la palabra clave volátil no puede garantizar la atomicidad de las operaciones.

En términos generales, el uso de volátiles debe cumplir las siguientes dos condiciones:

  1. Las operaciones de escritura en variables no dependen del valor actual

  2. La variable no se incluye en una invariante con otras variables.

De hecho, estas condiciones indican que los valores efectivos que se pueden escribir en variables volátiles son independientes del estado de cualquier programa, incluido el estado actual de la variable.

De hecho, tengo entendido que las dos condiciones anteriores deben garantizar que la operación sea atómica para garantizar que el programa que utiliza la palabra clave volátil se pueda ejecutar correctamente al mismo tiempo.

Escenarios comunes

  • Bandera de estado
volatile boolean flag = false;

while(!flag){
    doSomething();
}

public void setFlag() {
    flag = true;
}
  • Comprobación doble singleton
public class Singleton{
    private volatile static Singleton instance = null;

    private Singleton() {

    }

    public static Singleton getInstance() {
        if(instance==null) {
            synchronized (Singleton.class) {
                if(instance==null)
                    instance = new Singleton();
            }
        }
        return instance;
    }
}

Mejoras de JSR-133

En el antiguo modelo de memoria Java anterior a JSR-133, aunque no se permitía el reordenamiento entre variables volátiles, el antiguo modelo de memoria Java permitía reordenar entre variables volátiles y variables ordinarias.

En el modelo de memoria anterior, el programa de ejemplo VolatileExample se puede reordenar para que se ejecute en la siguiente secuencia:

class VolatileExample {
    int a = 0;
    volatile boolean flag = false;

    public void writer() {
        a = 1;                      //1
        flag = true;                //2
    }

    public void reader() {
        if (flag) {                //3
            int i =  a;            //4
        }
    }
}
  • cronología
时间线:----------------------------------------------------------------->
线程 A:(2)写 volatile 变量;                                  (1)修改共享变量 
线程 B:                    (3)读取 volatile 变量; (4)读共享变量

En el modelo de memoria antiguo, cuando no hay dependencia de datos entre 1 y 2, el reordenamiento entre 1 y 2 puede ser posible (3 y 4 son similares).

El resultado es: cuando el hilo del lector B ejecuta 4, es posible que no necesariamente vea la modificación de la variable compartida por el hilo del escritor A cuando ejecuta 1.

Por lo tanto, en el modelo de memoria antiguo, la escritura-lectura volátil no tiene la semántica de memoria de la adquisición de liberación del monitor.

Para proporcionar un mecanismo de comunicación entre subprocesos más liviano que los bloqueos de monitor,

El grupo de expertos JSR-133 decidió mejorar la semántica de memoria de volátiles:

Restrinja estrictamente el reordenamiento de variables volátiles y variables ordinarias por parte del compilador y el procesador, y asegúrese de que la escritura-lectura volátil y la adquisición de liberación del monitor tengan la misma semántica de memoria.

Desde la perspectiva de las reglas de reordenamiento del compilador y la estrategia de inserción de la barrera de memoria del procesador, siempre que el reordenamiento entre variables volátiles y variables ordinarias pueda destruir la semántica de memoria de volátiles,
este reordenamiento será reordenado por las barreras de memoria del compilador y del procesador. La política de inserción está prohibida.

principio de implementación volátil

Definición de términos

el termino Vocabulario inglés descripción
Variable compartida Variables compartidas Las variables que se pueden compartir entre varios subprocesos se denominan variables compartidas. Las variables compartidas incluyen todas las variables de instancia, variables estáticas y elementos de matriz. Todos están almacenados en la memoria del montón, los volátiles solo actúan sobre variables compartidas
Barrera de la memoria Barreras de memoria Es un conjunto de instrucciones del procesador que se utiliza para limitar el orden de las operaciones de memoria.
Línea de amortiguación Línea de caché La unidad de almacenamiento más pequeña que se puede asignar en la caché. Cuando el procesador llena la línea de caché, carga toda la línea de caché, lo que requiere varios ciclos de lectura de la memoria principal
Manipulación atómica Operaciones atómicas Una operación ininterrumpida o una serie de operaciones.
Relleno de línea de caché relleno de línea de caché Cuando el procesador reconoce que el operando leído de la memoria se puede almacenar en caché, el procesador lee toda la línea de caché en el caché apropiado (L1, L2, L3 o todos)
Golpe de caché golpe de caché Si la ubicación de la memoria para la operación de llenado de la línea de caché sigue siendo la dirección a la que accede el procesador la próxima vez, el procesador lee el operando del caché en lugar de leerlo de la memoria.
Escribir hit escribir hit Cuando el procesador vuelve a escribir el operando en un área de caché de memoria, primero verifica si la dirección de memoria de la caché está en la línea de caché. Si hay una línea de caché válida, el procesador vuelve a escribir el operando en la caché En lugar de volver a escribir en la memoria, esta operación se denomina acierto de escritura.
Desaparecido escribir pierde el caché Se escribe una línea de caché válida en un área de memoria que no existe

principio

Entonces, ¿cómo garantiza la visibilidad lo volátil?

Bajo el procesador x86, use la herramienta para obtener las instrucciones de ensamblaje generadas por el compilador JIT para ver qué hará la CPU al escribir volátiles.

  • Java
instance = new Singleton();//instance是volatile变量

Montaje correspondiente

0x01a3de1d: movb $0x0,0x1104800(%esi);
0x01a3de24: lock addl $0x0,(%esp);

Al escribir una variable compartida modificada con una variable volátil, habrá una segunda línea de código de ensamblaje.
Al consultar el manual del desarrollador de software de arquitectura IA-32, se puede saber que las lockinstrucciones prefijadas causarán dos cosas en procesadores de múltiples núcleos.

  • Los datos de la línea de caché del procesador actual se volverán a escribir en la memoria del sistema.

  • Esta operación de volver a escribir en la memoria invalidará los datos almacenados en caché en esa dirección de memoria en otras CPU.

Para mejorar la velocidad de procesamiento, el procesador no se comunica directamente con la memoria, sino que primero lee los datos de la memoria del sistema en el caché interno (L1, L2 u otro) antes de realizar la operación, pero después de la operación, no se sabe cuándo se escribirán en la memoria. ,

Si se declara una variable volátil para una operación de escritura, la JVM enviará una instrucción de prefijo de bloqueo al procesador para escribir los datos en la línea de caché donde la variable se encuentra de nuevo en la memoria del sistema.

Pero incluso si se vuelve a escribir en la memoria, si los valores en caché de otros procesadores aún son antiguos, habrá problemas al realizar operaciones de cálculo.

Por lo tanto, en los multiprocesadores, para asegurar que las cachés de cada procesador sean consistentes, se implementará un protocolo de coherencia de caché. Cada procesador verifica el valor de su propia caché olfateando los datos transmitidos en el bus para ver si el valor de su caché ha expirado.
Cuando el procesador descubre que la dirección de memoria correspondiente a su línea de caché ha sido modificada, establecerá la línea de caché del procesador actual en un estado no válido. Cuando el procesador quiera modificar los datos, forzará la recarga de los datos desde la memoria del sistema. Leer en la caché del procesador.

Visibilidad

Estas dos cosas se explican en detalle en el capítulo de administración de multiprocesador (Capítulo 8) del tercer volumen del Manual de arquitectura del desarrollador de software IA-32.

La instrucción Lock prefix hace que la caché del procesador se vuelva a escribir en la memoria.

La instrucción Lock prefix provoca la señal LOCK # del procesador de voz durante la ejecución de la instrucción.

En un entorno multiprocesador, la señal LOCK # asegura que el procesador pueda usar exclusivamente cualquier memoria compartida durante la afirmación de la señal. (Debido a que bloquea el bus, otras CPU no pueden acceder al bus. No acceder al bus significa que no se puede acceder a la memoria del sistema). Sin embargo, en los procesadores recientes, la señal LOCK # generalmente no bloquea el bus, pero bloquea el caché. La sobrecarga del autobús es relativamente grande.

Hay una descripción detallada del impacto de la operación de bloqueo en la caché del procesador en el capítulo 8.1.4 Para los procesadores Intel486 y Pentium, la señal LOCK # siempre se declara en el bus durante la operación de bloqueo.

Pero en P6 y procesadores recientes, si el área de memoria a la que se accede se almacena en caché dentro del procesador, la señal LOCK # no se confirmará.

Por el contrario, bloquea la caché de esta área de memoria y la vuelve a escribir en la memoria, y utiliza un mecanismo de coherencia de la caché para garantizar la atomicidad de la modificación. Esta operación se denomina "bloqueo de la caché". El
mecanismo de coherencia de la caché evita que se modifiquen modificaciones simultáneas. Datos del área de memoria almacenados en caché por dos o más procesadores.

Escribir el caché de un procesador en la memoria invalidará el caché de otros procesadores

Los procesadores IA-32 y los procesadores Intel 64 utilizan el protocolo de control MESI (modificar, excluir, compartir, invalidar) para mantener la coherencia de la caché interna y otras cachés de procesador.

Cuando operan en un sistema de procesador de múltiples núcleos, los procesadores IA-32 e Intel 64 pueden rastrear otros procesadores para acceder a la memoria del sistema y sus cachés internos.

Utilizan tecnología de rastreo para garantizar que los datos de su caché interno, la memoria del sistema y otros cachés del procesador permanezcan consistentes en el bus.

Por ejemplo, en los procesadores de la familia Pentium y P6, si se rastrea un procesador para detectar que otro procesador tiene la intención de escribir una dirección de memoria, y esta dirección se ocupa actualmente del estado compartido,
entonces el procesador que está rastreando invalidará su línea de caché. Al acceder a la misma dirección de memoria al mismo tiempo, se fuerza el llenado de la línea de caché.

Optimización del uso de volátiles

El conocido maestro de programación concurrente de Java Doug Lea agregó una clase de recopilación de colas al paquete concurrente de JDK7.Cuando usa variables volátiles LinkedTransferQueue,
utilizó una forma de agregar bytes para optimizar el rendimiento de la cola de cola y la puesta en cola.

¿Pueden los bytes adicionales optimizar el rendimiento? Este método se ve increíble, pero si comprende la arquitectura del procesador en profundidad, puede comprender el misterio.

Echemos un vistazo a LinkedTransferQueueesta clase,
utiliza tipos de clase internos para definir el encabezado de la cola de cola (Head) y el nodo de cola (tail),
y esta clase interna PaddedAtomicReference relativa a la clase principal AtomicReference solo hace una cosa, lo hará La variable compartida se agrega a 64 bytes .

Podemos calcular que la referencia a un objeto ocupa 4 bytes, y agrega 15 variables para ocupar un total de 60 bytes, más la variable Valor de la clase padre, lo que hace un total de 64 bytes.

  • LinkedTransferQueue.java
/** head of the queue */
private transient final PaddedAtomicReference < QNode > head;

/** tail of the queue */

private transient final PaddedAtomicReference < QNode > tail;

static final class PaddedAtomicReference < T > extends AtomicReference < T > {

    // enough padding for 64bytes with 4byte refs 
    Object p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, pa, pb, pc, pd, pe;

    PaddedAtomicReference(T r) {

        super(r);

    }

}

public class AtomicReference < V > implements java.io.Serializable {

    private volatile V value;

    //省略其他代码 
}

¿Por qué agregar 64 bytes puede mejorar la eficiencia de la programación concurrente?

Porque para los procesadores Intel Core i7, Core, Atom y NetBurst, Core Solo y Pentium M, la línea de caché de la caché L1, L2 o L3 tiene 64 bytes de ancho y no admite líneas de caché parcialmente llenas, lo que significa que si la cola es Si el nodo principal y el nodo de cola tienen menos de 64 bytes, el procesador los leerá todos en la misma línea de caché. En los multiprocesadores, cada procesador almacenará en caché los mismos nodos de cabeza y cola. Cuando un procesador intenta modificar El contacto de la cabeza bloqueará toda la línea de la caché, por lo que, bajo el efecto del mecanismo de coherencia de la caché, otros procesadores no podrán acceder al nodo de cola en su propia caché, y las operaciones de entrada y salida de cola deben modificar constantemente la cabeza. Juntas y nodos de cola, por lo que en el caso de multiprocesadores, afectará seriamente la eficiencia de entrada y salida de la cola.

Doug lea llena la línea de caché del búfer de alta velocidad agregando 64 bytes, evitando que el nodo principal y el nodo final se carguen en la misma línea de caché, de modo que los nodos principales y finales no se bloqueen entre sí cuando se modifiquen .

  • Entonces, ¿debería agregarse a 64 bytes cuando se usan variables volátiles?

No.

Este método no debe utilizarse en ambos escenarios.

Primero: para procesadores con líneas de caché que no tienen 64 bytes de ancho, como los procesadores de la serie P6 y Pentium, sus líneas de caché L1 y L2 tienen 32 bytes de ancho.

Segundo: las variables compartidas no se escribirán con frecuencia.

Debido a que el método de usar bytes adicionales requiere que el procesador lea más bytes en el búfer de alta velocidad, lo que en sí mismo traerá un cierto consumo de rendimiento, si la variable compartida no se escribe con frecuencia, la posibilidad de bloqueo también es muy pequeña. No es necesario agregar bytes para evitar el bloqueo mutuo.

PD: De repente siento que quiero especializarme en el campo del arte, el conocimiento y la sabiduría son indispensables.

hilo doble / largo no es seguro

Una de las muchas reglas definidas por la Especificación de máquina virtual Java: todas las operaciones en tipos básicos, excepto ciertas operaciones en tipos largos y dobles, son atómicas.

La JVM actual (máquina virtual Java) utiliza 32 bits como operaciones atómicas, no 64 bits.

Cuando el subproceso lee el valor de tipo largo / doble en la memoria principal en la memoria del subproceso, pueden ser dos operaciones de escritura del valor de 32 bits. Obviamente, si varios subprocesos operan al mismo tiempo, entonces puede haber dos 32 bits alto y bajo. Se produce un error de valor.

Para compartir campos largos y dobles entre subprocesos, deben operarse sincronizados o declarados como volátiles.

resumen

Volátil, como palabra clave muy importante en JMM, es básicamente un punto de conocimiento que se debe solicitar para entrevistas de alta concurrencia.

Espero que este artículo sea de ayuda para tu entrevista de trabajo y estudio. Si tienes otras ideas, también puedes compartirlas contigo en la sección de comentarios.

Los gustos, favoritos y reenvíos de los geeks son la mayor motivación para la escritura de Ma.

Para obtener más contenido interesante, puede seguir a [Lao Ma Xiaoxifeng]

Después de trabajar durante 5 años, ¿ni siquiera conozco la palabra clave volátil?

Supongo que te gusta

Origin blog.51cto.com/9250070/2542681
Recomendado
Clasificación