El contenido principal de este artículo es un punto de conocimiento común en la entrevista de Java: palabras clave volátiles. Este artículo presenta todos los aspectos de las palabras clave volátiles en detalle. Espero que después de leer este artículo, pueda resolver perfectamente los problemas relacionados de las palabras clave volátiles.
En entrevistas de trabajo relacionadas con Java, a muchos entrevistadores les gusta examinar la comprensión del entrevistador de la concurrencia de Java. Usando la palabra clave volátil como un pequeño punto de entrada, a menudo puede preguntarle al modelo de memoria Java (JMM) y algunas características de la programación concurrente de Java. En profundidad, también puede examinar la implementación subyacente de JVM y el conocimiento relacionado con el sistema operativo. ¡Tomemos un proceso de entrevista hipotética para obtener una comprensión profunda de la palabra clave Volitile!
Por lo que entiendo, las variables compartidas modificadas por Volatile tienen las siguientes dos características:
1. Asegurar la visibilidad de la memoria de diferentes hilos a la operación variable;
2. Prohibir el reordenamiento del comando
Esto es mucho de qué hablar, así que comenzaré con el modelo de memoria Java. La especificación de máquina virtual Java intenta definir un modelo de memoria Java (JMM) para bloquear las diferencias de acceso a la memoria entre varios sistemas de hardware y operaciones, de modo que los programas de Java pueden lograr efectos consistentes de acceso a la memoria en varias plataformas. En pocas palabras, dado que la CPU ejecuta instrucciones muy rápidamente, la velocidad de acceso a la memoria es mucho más lenta, y la diferencia no es un orden de magnitud, los grandes que trabajan en el procesador han agregado varias capas de caché a la CPU. En el modelo de memoria Java, la optimización anterior se abstrae nuevamente. JMM estipula que todas las variables están en la memoria principal, similar a la memoria ordinaria mencionada anteriormente, y cada hilo contiene su propia memoria de trabajo. Se puede considerar como un registro o caché en la CPU para una fácil comprensión. Por lo tanto, las operaciones de hilo se basan principalmente en la memoria de trabajo. Solo pueden acceder a su propia memoria de trabajo, y deben sincronizar el valor de nuevo a la memoria principal antes y después del trabajo. Ni siquiera sé lo que dije, toma un trozo de papel para dibujar:
Al ejecutar un hilo, el valor de la variable se leerá primero desde la memoria principal, luego se cargará a la copia en la memoria de trabajo y luego lo pasará al procesador para su ejecución. Una vez completada la ejecución, a la copia en la memoria de trabajo se le asignará un valor, y luego el valor en la memoria de trabajo se volverá a pasar a la memoria principal, y el valor en la memoria principal se actualizará. Aunque el uso de la memoria de trabajo y la memoria principal es más rápido, también trae algunos problemas. Por ejemplo, mire el siguiente ejemplo:
i = i + 1;
Suponiendo que el valor inicial de I es 0, cuando solo un hilo lo ejecuta, el resultado definitivamente obtendrá 1. Cuando se ejecuten dos hilos, ¿obtendrá el resultado 2? Este no es necesariamente el caso. Este puede ser el caso:
Hilo 1: Cargar I de la memoria principal // i = 0 i + 1 // i = 1 hilo 2: Cargar I de la memoria principal // porque el hilo 1 no ha escrito el valor de I de vuelta a la memoria principal, todavía es 0 i + 1 // i = 1 hilo 1: Guardar I a la memoria principal 2: Guardar la memoria principal a la memoria principal
Si dos hilos siguen el proceso de ejecución anterior, entonces el último valor de I es en realidad 1. Si la última escritura es lenta, y puede leer el valor de I nuevamente, puede ser 0, que es el problema de inconsistencia de caché. La siguiente es mencionar la pregunta que acabas de hacer. JMM se establece principalmente sobre cómo lidiar con las tres características de atomicidad, visibilidad y orden en el proceso de concurrencia. Al resolver estos tres problemas, se puede resolver el problema de la inconsistencia del caché. Y el volátil está relacionado con la visibilidad y el orden.
1. Atomicidad: en Java, las operaciones de lectura y asignación de tipos de datos básicos son operaciones atómicas. Las llamadas operaciones atómicas significan que estas operaciones son ininterrumpibles y deben completarse por un cierto período de tiempo, o no se ejecutarán. Por ejemplo:
i = 2; j = i; i ++; i = i+1;
Entre las cuatro operaciones anteriores, i = 2 es una operación de lectura, que debe ser una operación atómica. J = Creo que es una operación atómica. De hecho, se divide en dos pasos. Una es leer el valor de I, y luego asignar el valor a j. Esta es una operación de 2 pasos. No se puede llamar una operación atómica. i ++ e i = i+ 1 son realmente equivalentes. Lea el valor de I, agregue 1 y vuelva a escribirlo a la memoria principal. Esa es una operación de 3 pasos. Por lo tanto, en el ejemplo anterior, el último valor puede tener muchas situaciones porque no puede satisfacer la atomicidad. De esta manera, solo hay una lectura simple. La asignación es una operación atómica o solo una asignación numérica. Si usa variables, hay una operación adicional para leer el valor variable. Una excepción es que la especificación de la máquina virtual permite que los tipos de datos de 64 bits (largo y doble) se procesen en 2 operaciones, pero la última implementación de JDK aún implementa operaciones atómicas. JMM solo implementa la atomicidad básica. Las operaciones como el I ++ anterior deben sincronizarse y bloquearse para garantizar la atomicidad de todo el código. Antes de que el hilo libere el bloqueo, inevitablemente cepillará el valor de I de vuelta a la memoria principal. 2. Visibilidad: Hablando de visibilidad, Java usa volátile para proporcionar visibilidad. Cuando Volatile modifica una variable, la modificación se actualizará inmediatamente a la memoria principal. Cuando otros hilos necesitan leer la variable, el nuevo valor se leerá en la memoria. Esto no está garantizado por variables ordinarias. De hecho, sincronizado y bloqueo también puede garantizar la visibilidad. Antes de que el hilo libere el bloqueo, enjuagará todos los valores variables compartidos a la memoria principal, pero sincronizados y el bloqueo son más caros. 3. Ordenar JMM permite que el compilador y el procesador reordenen las instrucciones, pero estipula la semántica de AS-if-Serial, es decir, sin importar cuán reordenamiento, el resultado de la ejecución del programa no se puede cambiar. Por ejemplo, el siguiente segmento del programa:
doble pi = 3.14; // Adeoble R = 1; // bdouble s = pi * r * r; // c
La declaración anterior se puede ejecutar en A-> B-> C, con el resultado de 3.14, pero también se puede ejecutar en el orden de b-> a-> c. Debido a que A y B son dos declaraciones independientes, mientras que C depende de A y B, A y B se pueden reordenar, pero C no se puede clasificar primero en A y B. JMM asegura que el reordenamiento no afecte la ejecución de un solo hilo, pero los problemas son propensos a ocurrir en el rendimiento múltiple. Por ejemplo, código como este:
int a = 0; bool flag = false; public void write () {a = 2; // 1 bandera = verdadero; // 2} public void multiply () {if (flag) {// 3 int ret = a * a; // 4}}Si dos subprocesos ejecutan el segmento de código anterior, Thread 1 primero ejecuta Write, entonces Thread 2 luego se ejecuta Multiplicar, ¿el valor de RET debe ser 4? El resultado no es necesariamente:
Como se muestra en la figura, se reordenan 1 y 2 en el método de escritura. El hilo 1 primero asigna el indicador a verdadero, luego lo ejecuta al hilo 2, RET calcula directamente el resultado y luego al subproceso 1. En este momento, A se asigna a 2, que obviamente es un paso más tarde. En este momento, puede agregar la palabra clave volátil a la bandera, prohibir el reordenamiento, lo que puede garantizar el orden del programa, y también puede usar sincronizado y bloqueo de peso pesado para garantizar el orden. Pueden asegurarse de que el código en esa área se ejecute a la vez. Además, JMM tiene un orden innato, es decir, el orden que se puede garantizar sin ningún medio, que generalmente se llama el principio de antes. << JSR-133: Modelo de memoria de Java y especificación de subprocesos >> Define las siguientes reglas de homenaje: 1. Reglas de secuencia del programa: Para cada operación en un subproceso, ocurre antes de cualquier operación posterior a las operaciones posteriores en el subproceso 2. Reglas de bloqueo de monitor: desbloquear un subproceso, ocurre antes para obtener la lectura posterior de este subproceso 3. Reglas de variables volátiles: Reglas de variables volátiles: Escribir una vultain Domain, ocurre el bisorbente de la ventaja subsuperta para la lectura posterior para la lectura posterior para la lectura posterior para la lectura posterior de la lectura de este subproceso 3. Dominio volátil 4. Transitividad: si A Ocurre B y B ocurre antes de C, entonces A HACEBRE AFORE C 5.Start () Reglas: si el subproceso A realiza una operación Threadb_Start () (Iniciar hilo B), entonces Threadb_start () ocurre antes de la operación arbitraria de Thread A en B 6.JoJoin () Principle: si es ejecutivo. Subprota de subproceso regresa con éxito de la operación Threadb.Join () en Thread A. 7. Interrupt () Principio: La llamada al método de interrupción de hilo () ocurre primero cuando el código de hilo interrumpido detecta el evento de interrupción. Puede usar el método Thread.interrupted () para detectar si hay una interrupción. 8. Principio finalize (): la finalización de la inicialización de un objeto ocurre primero cuando comienza el método finalize (). La primera regla de la regla de secuencia del programa dice que en un hilo, todas las operaciones están en secuencia, pero en JMM, siempre que el resultado de la ejecución sea el mismo, se permite reordenar. El enfoque de HACE antes de aquí también es la exactitud del resultado de la ejecución de un solo hilo, pero no se puede garantizar que lo mismo es cierto para el múltiple subproceso. Regla 2 Las reglas del monitor son realmente fáciles de entender. Antes de agregar el bloqueo, solo puede continuar agregando el bloqueo. La regla 3 se aplica al volátil en cuestión. Si un hilo escribe una variable primero y otro hilo lo lee, entonces la operación de escritura debe ser antes de la operación de lectura. La cuarta regla es la transitividad de Happens-Before. No entraré en detalles sobre los siguientes.
Luego necesitamos que las reglas de variables volátiles vuelvas a mencionar: escriba un dominio volátil, antes de leer este dominio volátil más adelante. Déjame sacar esto de nuevo. De hecho, si una variable se declara como volátil, entonces cuando leo la variable, siempre puedo leer su último valor. Aquí, el último valor significa que no importa qué hilo escriba la variable, se actualizará a la memoria principal de inmediato. También puedo leer el valor recién escrito de la memoria principal. En otras palabras, la palabra clave volátil puede garantizar la visibilidad y el orden. Tomemos el código anterior como ejemplo:
int a = 0; bool flag = false; public void write () {a = 2; // 1 bandera = verdadero; // 2} public void multiply () {if (flag) {// 3 int ret = a * a; // 4}}Este código no solo se preocupa por el reordenamiento, incluso si 1 y 2 no están reordenados. 3 tampoco se ejecutará tan bien. Suponga que el subproceso 1 ejecuta primero la operación de escritura, y el subproceso 2 realiza la operación de multiplicación. Dado que el hilo 1 asigna la bandera a 1 en la memoria de trabajo, no se puede volver a escribir a la memoria principal de inmediato. Por lo tanto, cuando el subproceso 2 se ejecuta, Multiply lee el valor del indicador de la memoria principal, que aún puede ser falso, por lo que las declaraciones en los soportes no se ejecutarán. Si se cambia a lo siguiente:
int a = 0; volátil bool bandera = false; public void write () {a = 2; // 1 bandera = verdadero; // 2} public void multiply () {if (flag) {// 3 int ret = a * a; // 4}}Luego, el hilo 1 ejecuta escribir primero, y el hilo 2 luego ejecuta multiplicar. De acuerdo con el principio, este proceso satisfará los siguientes tres tipos de reglas: Reglas de orden del programa: 1 Ocurre antes 2; 3 sucede antes 4; (Volátil restringe el reordenamiento de instrucciones, por lo que 1 se ejecuta antes de 2) Reglas volátiles: 2 sucede antes de 3 Reglas transitivas: 1 sucede antes de 4 Al escribir una variable volátil, JMM descargará la variable compartida en la memoria local correspondiente al hilo a la memoria principal. Al leer una variable volátil, JMM establecerá la memoria local correspondiente al hilo para invalidar, y el hilo leerá la variable compartida de la memoria principal a continuación.
En primer lugar, mi respuesta es que la atomicidad no se puede garantizar. Si está garantizado, es solo una atomicidad para la lectura/escritura de una sola variable volátil, pero no hay nada que ver con las operaciones compuestas como Volatile ++, como el siguiente ejemplo:
Prueba de clase pública {public Volatile int Inc = 0; public void aumento () {inc ++; } public static void main (string [] args) {fin final test = new test (); for (int i = 0; i <10; i ++) {new Thread () {public void run () {for (int j = 0; j <1000; j ++) test.increase (); }; }.comenzar(); } while (thread.activeCount ()> 1) // Asegúrese de que los hilos anteriores hayan completado thread.yield (); System.out.println (test.inc); }Hablando lógicamente, el resultado es de 10,000, pero es probable que sea un valor inferior a 10,000 cuando se ejecuta. Algunas personas pueden decir que Volátil no garantiza la visibilidad. Un hilo debe ver las modificaciones a Inc por Inc, ¡y el otro hilo debe verlo inmediatamente! Pero la Operación Inc ++ aquí es una operación compuesta, que incluye leer el valor de Inc, aumentarlo por sí mismo y luego volver a escribirla a la memoria principal. Supongamos que el hilo A lee el valor de Inc para que sea 10, y se bloquea en este momento porque la variable no se modifica y la regla volátil no puede activarse. El hilo B también lee el valor de Inc en este momento. El valor de INC en la memoria principal sigue siendo 10, y se incrementará automáticamente, y luego se volverá a escribir a la memoria principal, que es 11. En este momento, es el turno de hilo A para ejecutarse. Dado que 10 se guarda en la memoria de trabajo, continúa aumentando y escribe a la memoria principal. 11 está escrito de nuevo. Entonces, aunque los dos hilos ejecutados aumentan () dos veces, solo agregaron una vez. Algunas personas dicen: ¿Volátil no invalida la línea de caché? Sin embargo, antes de que el hilo A lea el subproceso B y realice operaciones, el valor de Inc no se modifica, por lo que cuando el hilo B lee, todavía lee 10. Algunas personas también dicen que si el hilo B escribe 11 de nuevo a la memoria principal, ¿no establecerá la línea de caché del hilo A para invalidar? Sin embargo, el hilo A ya ha realizado la operación de lectura. Solo cuando se realice la operación de lectura y la línea de caché no sea válida, leerá el valor de memoria principal. Por lo tanto, el hilo A solo puede continuar haciendo autoincremento. En resumen, en este tipo de operación compuesta, la función atómica no se puede mantener. Sin embargo, en el ejemplo anterior de configuración del valor del indicador, ya que la operación de lectura/escritura de los indicadores es de un solo paso, aún puede garantizar la atomicidad. Para garantizar la atomicidad, solo podemos usar clases de operación atómica sincronizadas, de bloqueo y atómica en paquetes concurrentes, es decir, el auto-incremento (operación Agregar 1), autodecruento (reducción de la operación 1), operación de suma (agregar un número) y la operación de resta (reste un número) de los tipos de datos básicos para garantizar que estas operaciones sean operaciones atómicas.
Si genera código de ensamblaje con la palabra clave volátil y el código sin la palabra clave volátil, encontrará que el código con la palabra clave volátil tendrá una instrucción de prefijo de bloqueo adicional. La instrucción del prefijo de bloqueo es realmente equivalente a una barrera de memoria. La barrera de memoria proporciona las siguientes funciones: 1. Al reordenar, las siguientes instrucciones no se pueden reordenar a la ubicación antes de la barrera de memoria 2. Haga el caché de esta CPU escrito a la memoria ** ** 3. La acción de escritura también causará que otros CPU u otros núcleos invaliden su caché, que es equivalente a hacer que el valor nuevo escrito sea visible a otros hilos.
1. Marca de cantidad de estado, al igual que la bandera anterior, lo mencionaré nuevamente:
int a = 0; volátil bool bandera = false; public void write () {a = 2; // 1 bandera = verdadero; // 2} public void multiply () {if (flag) {// 3 int ret = a * a; // 4}}Esta operación de lectura y escritura a variables, marcadas como volátiles, puede garantizar que las modificaciones sean visibles inmediatamente al hilo. En comparación con sincronizado, el bloqueo tiene una cierta mejora de la eficiencia. 2. Implementación del modo Singleton, bloqueo de doble verificación típico (DCL)
clase Singleton {Instancia singleton static static volátil privada = nulo; Private Singleton () {} public static singleton getInstance () {if (instance == null) {sincronizado (singleton.class) {if (instance == null) instancia = nueva singleton (); }} instancia de retorno; }}Este es un patrón de singleton perezoso, los objetos se crean solo cuando se usan y para evitar reordenar las instrucciones para las operaciones de inicialización, se agrega volátil a la instancia.
Lo anterior es todo el contenido de este artículo sobre la explicación de las palabras clave volátiles que los entrevistadores de Java les encanta preguntar en detalle. Espero que sea útil para todos. Los amigos interesados pueden continuar referiéndose a otros temas relacionados en este sitio. Si hay alguna deficiencia, deje un mensaje para señalarlo. ¡Gracias amigos por su apoyo para este sitio!