Muchos amigos pueden haber oído hablar de la palabra clave volátil y pueden haberla usado. Antes de Java 5, era una palabra clave controvertida, ya que usarla en programas a menudo resultaba en resultados inesperados. Solo después de Java 5, la palabra clave volátil recuperó su vitalidad.
Aunque la palabra clave volátil es literalmente simple de entender, no es fácil usarla bien. Dado que la palabra clave volátil está relacionada con el modelo de memoria de Java, antes de decirle la clave volátil, primero entendemos los conceptos y el conocimiento relacionados con el modelo de memoria, luego analizamos el principio de implementación de la palabra clave volátil y finalmente damos varios escenarios de uso de la palabra clave volátil.
Aquí está el esquema del directorio de este artículo:
1. Conceptos relacionados de modelos de memoria
Como todos sabemos, cuando una computadora ejecuta un programa, cada instrucción se ejecuta en la CPU, y durante la ejecución de la instrucción, inevitablemente implicará la lectura y la redacción de datos. Dado que los datos temporales durante la operación del programa se almacenan en la memoria principal (memoria física), hay un problema en este momento. Dado que la velocidad de ejecución de la CPU es muy rápida, el proceso de leer datos de la memoria y escribir datos a la memoria es mucho más lento que la ejecución de instrucciones de la CPU. Por lo tanto, si la operación de datos debe llevarse a cabo a través de la interacción con la memoria en cualquier momento, la velocidad de ejecución de la instrucción se reducirá considerablemente. Por lo tanto, hay un caché en la CPU.
Es decir, cuando el programa se esté ejecutando, copiará los datos requeridos por la operación de la memoria principal a la caché de la CPU. Luego, cuando la CPU realiza cálculos, puede leer directamente los datos de su caché y escribir datos. Después de completar la operación, los datos en el caché se enjuagarán en la memoria principal. Demos un ejemplo simple, como el siguiente código:
i = i + 1;
Cuando el hilo ejecuta esta declaración, primero leerá el valor de I desde la memoria principal, luego copiará una copia en el caché, y luego la CPU ejecutará la instrucción para agregar 1 a I, luego escribir los datos en el caché y finalmente actualizar el último valor de i en la memoria caché a la memoria principal.
No hay problema con este código que se ejecuta en un solo hilo, pero habrá problemas al ejecutarse en un subproceso múltiple. En las CPU múltiples, cada hilo puede ejecutarse en una CPU diferente, por lo que cada hilo tiene su propio caché al ejecutar (para CPU de un solo núcleo, este problema realmente ocurrirá, pero se ejecuta por separado en forma de programación de hilos). En este artículo, tomamos CPU de múltiples núcleos como ejemplo.
Por ejemplo, dos hilos ejecutan este código al mismo tiempo. Si el valor de I es 0 al principio, esperamos que el valor de I se convierta en 2 después de que los dos hilos se hayan ejecutado. ¿Pero será este el caso?
Puede haber una de las siguientes situaciones: al principio, dos hilos leen el valor de I y almacenanlo en el caché de sus respectivas CPU, y luego el hilo 1 realiza una operación de agregar 1, y luego escribe el último valor de I a la memoria. En este momento, el valor de I en el caché del subproceso 2 sigue siendo 0. Después de realizar la operación 1, el valor de I es 1, y luego el hilo 2 escribe el valor de I a la memoria.
El valor del resultado final I es 1, no 2. Este es el famoso problema de consistencia de caché. Esta variable a la que se accede por múltiples hilos generalmente se llama una variable compartida.
Es decir, si una variable se almacena en múltiples CPU (generalmente solo ocurre durante la programación de lectura múltiple), entonces puede haber un problema de inconsistencia de caché.
Para resolver el problema de inconsistencia de caché, generalmente hay dos soluciones:
1) Agregar bloqueo de bloqueo de bloqueo al bus
2) a través del protocolo de coherencia de caché
Estos dos métodos se proporcionan a nivel de hardware.
En las CPU tempranas, el problema de la inconsistencia de la caché se resolvió agregando bloqueos de# de bloqueo al autobús. Debido a que la comunicación entre la CPU y otros componentes se lleva a cabo a través del bus, si el bus se agrega con un bloqueo de bloqueo de bloqueo, significa que otras CPU están bloqueadas para acceder a otros componentes (como la memoria), de modo que solo una CPU pueda usar la memoria de esta variable. Por ejemplo, en el ejemplo anterior, si un hilo está ejecutando i = i +1, y si la señal de bloqueo de# LCOK se envía en el bus durante la ejecución de este código, solo después de esperar que el código se ejecute completamente, otras CPU pueden leer la variable desde la memoria donde la variable I se encuentra y luego realizar las operaciones correspondientes. Esto resuelve el problema de la inconsistencia del caché.
Pero el método anterior tendrá un problema, porque otras CPU no pueden acceder a la memoria durante el bloqueo del bus, lo que resulta en ineficiencia.
Entonces surge un protocolo de consistencia de caché. El más famoso es el protocolo MESI de Intel, que garantiza que la copia de las variables compartidas utilizadas en cada caché sea consistente. Su idea central es: cuando la CPU escribe datos, si encuentra que la variable que está operada es una variable compartida, es decir, hay una copia de la variable en otras CPU, indicará a otras CPU para establecer la línea de caché de la variable a un estado inválido. Por lo tanto, cuando otras CPU deben leer esta variable y encontrar que la línea de caché que almacena en caché la variable en su caché no es válida, entonces volverá a leer de la memoria.
2. Tres conceptos en programación concurrente
En la programación concurrente, generalmente encontramos los siguientes tres problemas: problema de atomicidad, problema de visibilidad y problema ordenado. Echemos un vistazo a estos tres conceptos primero:
1. Atomicidad
Atomicidad: es decir, una operación o múltiples operaciones se ejecutan todo y el proceso de ejecución no será interrumpido por ningún factor, o no se ejecutará.
Un ejemplo muy clásico es el problema de transferencia de la cuenta bancaria:
Por ejemplo, si transfiere 1,000 yuanes de la cuenta A a la cuenta B, inevitablemente incluirá 2 operaciones: reste 1,000 yuanes de la cuenta A y agregue 1,000 yuanes a la cuenta B.
Solo imagine qué consecuencias se causarán si estas dos operaciones no son atómicas. Si se restan 1,000 yuanes de la cuenta A, la operación se terminará repentinamente. Luego, 500 yuanes fueron retirados de B, y después de retirar 500 yuanes, luego la operación de agregar 1,000 yuanes a la cuenta B. Esto llevará al hecho de que, aunque la cuenta A tiene menos 1,000 yuanes, la cuenta B no ha recibido los 1,000 yuanes transferidos.
Por lo tanto, estas dos operaciones deben ser atómicas para garantizar que no haya problemas inesperados.
¿Cuáles son los resultados que se reflejarán en la programación concurrente?
Para dar el ejemplo más simple, piense en lo que sucedería si el proceso de asignar una variable de 32 bits no es atómico.
i = 9;
Si un subproceso ejecuta esta declaración, asumiré que la asignación de una variable de 32 bits incluye dos procesos: asignación de un menor de 16 bits y asignación de un mayor de 16 bits.
Luego puede ocurrir una situación: cuando se escribe el bajo valor de 16 bits, se interrumpe de repente, y en este momento otro hilo lee el valor de I, entonces lo que se lee son los datos incorrectos.
2. Visibilidad
La visibilidad se refiere a cuando múltiples subprocesos acceden a la misma variable, un hilo modifica el valor de la variable y otros subprocesos pueden ver inmediatamente el valor modificado.
Para un ejemplo simple, consulte el siguiente código:
// El código ejecutado por el hilo 1 es int i = 0; i = 10; // El código ejecutado por el hilo 2 es j = i;
Si el hilo de ejecución 1 es CPU1 y el hilo de ejecución 2 es CPU2. En el análisis anterior, podemos ver que cuando Thread 1 ejecuta la oración i = 10, el valor inicial de I se cargará en el caché de CPU1 y luego se le asigna un valor de 10. Luego, el valor de I en el caché de CPU1 se convierte en 10, pero no se escribe inmediatamente en la memoria principal.
En este momento, Thread 2 ejecuta j = I, y primero irá a la memoria principal para leer el valor de I y cargarlo en el caché de CPU2. Tenga en cuenta que el valor de I en la memoria sigue siendo 0, por lo que el valor de J será 0, no 10.
Este es el problema de visibilidad. Después de que el hilo 1 modifica la variable I, el hilo 2 no ve inmediatamente el valor modificado por hilo 1.
3. Orden
Orden: es decir, el orden de ejecución de los programas se ejecuta en el orden del código. Para un ejemplo simple, consulte el siguiente código:
int i = 0; bandera booleana = falso; i = 1; // Declaración 1 Bandera = True; // Declaración 2
El código anterior define una variable de tipo INT, una variable de tipo booleana, y luego asigna valores a las dos variables respectivamente. Desde la perspectiva de la secuencia del código, la declaración 1 es antes de la Declaración 2. Entonces, cuando el JVM ejecute este código, ¿asegurará que la Declaración 1 se ejecute antes de la Declaración 2? No necesariamente, ¿por qué? Reorden de instrucciones puede ocurrir aquí.
Expliquemos qué es el reordenamiento de instrucciones. En términos generales, para mejorar la eficiencia de la operación del programa, el procesador puede optimizar el código de entrada. No garantiza que la orden de ejecución de cada declaración en el programa sea consistente con el orden en el código, pero asegurará que el resultado de ejecución final del programa y el resultado de la secuencia de ejecución del código sean consistentes.
Por ejemplo, en el código anterior, que ejecuta la declaración 1 y la declaración 2 primero no tiene ningún efecto en el resultado final del programa, entonces es posible que durante el proceso de ejecución, la declaración 2 se ejecute primero y la declaración 1 se ejecute más adelante.
Pero tenga en cuenta que, aunque el procesador reordenará las instrucciones, asegurará que el resultado final del programa sea el mismo que la secuencia de ejecución del código. Entonces, ¿qué garantiza que sea? Echemos un vistazo al siguiente ejemplo:
int a = 10; // Declaración 1int r = 2; // Declaración 2a = a + 3; // Declaración 3r = a*a; // Declaración 4
Este código tiene 4 declaraciones, por lo que una posible orden de ejecución es:
Por lo tanto, es posible ser la orden de ejecución: Declaración 2 Declaración 1 Declaración 4 3
No es posible porque el procesador considerará la dependencia de los datos entre las instrucciones al reordenar. Si una instrucción de instrucción 2 debe usar el resultado de la Instrucción 1, el procesador se asegurará de que la Instrucción 1 se ejecute antes de la Instrucción 2.
Aunque el reordenamiento no afectará los resultados de la ejecución del programa dentro de un solo hilo, ¿qué pasa con MultIThInsreading? Veamos un ejemplo a continuación:
// hilo 1: context = loadContext (); // estado 1inited = true; // estado 2 // hilo 2: while (! Inited) {sleep ()} dosomethingwithConfig (contexto);En el código anterior, dado que las declaraciones 1 y 2 no tienen dependencias de datos, se pueden reordenar. Si se produce el reordenamiento, la declaración 2 se ejecuta primero durante la ejecución del hilo 1, y este es el hilo 2 pensará que el trabajo de inicialización se ha completado, y luego saltará del bucle while para ejecutar el método DoSomethingwithConfig (context). En este momento, el contexto no se inicializa, lo que causará un error del programa.
Como se puede ver en lo anterior, el reordenamiento de instrucciones no afectará la ejecución de un solo hilo, sino que afectará la corrección de la ejecución concurrente de los hilos.
En otras palabras, para ejecutar programas concurrentes correctamente, se debe garantizar la atomicidad, la visibilidad y el orden. Mientras uno no esté garantizado, puede hacer que el programa se ejecute incorrectamente.
3. Modelo de memoria de Java
Hablé sobre algunos problemas que pueden surgir en los modelos de memoria y la programación concurrente. Echemos un vistazo al modelo de memoria de Java y estudiemos qué garantiza que el modelo de memoria Java nos proporciona y qué métodos y mecanismos se proporcionan en Java para garantizar la corrección de la ejecución del programa al realizar una programación multiproceso.
En la especificación de la máquina virtual Java, se intenta definir un modelo de memoria Java (JMM) para bloquear las diferencias de acceso a la memoria entre varias plataformas de hardware y sistemas operativos, para permitir que los programas Java logren efectos consistentes de acceso a la memoria en varias plataformas. Entonces, ¿qué estipula el modelo de memoria Java? Define las reglas de acceso para las variables en un programa. Para decirlo más ampliamente, define el orden de ejecución del programa. Tenga en cuenta que para obtener un mejor rendimiento de ejecución, el modelo de memoria Java no restringe que el motor de ejecución utilice los registros o cachés del procesador para mejorar la velocidad de ejecución de las instrucciones, ni restringe al compilador a reordenar las instrucciones. En otras palabras, en el modelo de memoria Java, también habrá problemas de consistencia de caché y problemas de reordenamiento de instrucciones.
El modelo de memoria Java estipula que todas las variables están en la memoria principal (similar a la memoria física mencionada anteriormente), y cada hilo tiene su propia memoria de trabajo (similar al caché anterior). Todas las operaciones de un hilo en una variable deben realizarse en la memoria de trabajo, y no pueden funcionar directamente en la memoria principal. Y cada hilo no puede acceder a la memoria de trabajo de otros hilos.
Para dar un ejemplo simple: en Java, ejecute la siguiente declaración:
i = 10;
El hilo de ejecución primero debe asignar la línea de caché donde la variable I se encuentra en su propio hilo de trabajo, y luego escribirlo en la memoria principal. En lugar de escribir el valor 10 directamente en la memoria principal.
Entonces, ¿qué garantías proporciona el lenguaje Java para la atomicidad, la visibilidad y el orden?
1. Atomicidad
En Java, las operaciones de lectura y asignación de variables de tipos de datos básicos son operaciones atómicas, es decir, estas operaciones no pueden interrumpirse y ejecutar o no.
Aunque la oración anterior parece simple, no es tan fácil de entender. Vea el siguiente ejemplo I:
Analice cuáles de las siguientes operaciones son operaciones atómicas:
x = 10; // Declaración 1y = x; // Declaración 2x ++; // Declaración 3x = x + 1; // Declaración 4
A primera vista, algunos amigos pueden decir que las operaciones en las cuatro declaraciones anteriores son todas las operaciones atómicas. De hecho, solo la declaración 1 es una operación atómica, y ninguna de las otras tres declaraciones son operaciones atómicas.
La declaración 1 asigna directamente el valor 10 a x, lo que significa que el hilo ejecuta esta declaración y escribe el valor 10 directamente en la memoria de trabajo.
La declaración 2 en realidad contiene 2 operaciones. Primero necesita leer el valor de X, y luego escribir el valor de X a la memoria de trabajo. Aunque las dos operaciones de leer el valor de x y escribir el valor de x a la memoria de trabajo son operaciones atómicas, no son operaciones atómicas juntas.
Del mismo modo, x ++ y x = x+1 incluyen 3 operaciones: lea el valor de x, realice el funcionamiento de agregar 1 y escriba el nuevo valor.
Por lo tanto, solo el funcionamiento de la declaración 1 en las cuatro declaraciones anteriores es atómica.
En otras palabras, solo la lectura y la asignación simples (y el número debe asignarse a una variable, y la asignación mutua entre variables no es una operación atómica) es una operación atómica.
Sin embargo, hay una cosa que tener en cuenta aquí: bajo la plataforma de 32 bits, la lectura y la asignación de datos de 64 bits deben completarse a través de dos operaciones, y su atomicidad no puede garantizarse. Sin embargo, parece que en el último JDK, el JVM ha asegurado que la lectura y la asignación de datos de 64 bits también sea una operación atómica.
De lo anterior, se puede ver que el modelo de memoria Java solo garantiza que las lecturas y tareas básicas sean operaciones atómicas. Si desea lograr una atomicidad de una gama más amplia de operaciones, se puede lograr a través de sincronizado y bloqueo. Dado que sincronizado y bloqueo puede garantizar que solo un hilo ejecute el bloque de código en cualquier momento, naturalmente no habrá ningún problema de atomicidad, lo que garantiza la atomicidad.
2. Visibilidad
Para la visibilidad, Java proporciona la palabra clave volátil para garantizar la visibilidad.
Cuando Volatile modifica una variable compartida, asegura que el valor modificado se actualizará a la memoria principal de inmediato, y cuando otros hilos necesiten leerlo, leerá el nuevo valor en la memoria.
Sin embargo, las variables compartidas ordinarias no pueden garantizar la visibilidad, porque no está claro cuándo se escribe la variable compartida normal en la memoria principal después de modificarse. Cuando otros hilos lo leen, el valor antiguo original aún puede estar en la memoria, por lo que no se puede garantizar la visibilidad.
Además, sincronizado y bloqueo también puede garantizar la visibilidad. Sincronized and Lock puede garantizar que solo un hilo adquiere el bloqueo al mismo tiempo y ejecute el código de sincronización. Antes de liberar el bloqueo, la modificación de la variable se actualizará a la memoria principal. Por lo tanto, la visibilidad se puede garantizar.
3. Orden
En el modelo de memoria Java, los compiladores y los procesadores pueden reordenar las instrucciones, pero el proceso de reordenamiento no afectará la ejecución de programas de un solo subproceso, sino que afectará la corrección de la ejecución concurrente multiproceso.
En Java, se puede garantizar una cierta "línea de orden" a través de la palabra clave volátil (el principio específico se explica en la siguiente sección). Además, se pueden usar sincronizado y bloqueo para garantizar el orden. Obviamente, sincronizado y bloqueo asegura que haya un hilo que ejecute el código de sincronización en cada momento, lo que es equivalente a dejar que los hilos ejecuten el código de sincronización en secuencia, lo que naturalmente garantiza el orden.
Además, el modelo de memoria de Java tiene una "línea de orden" innata, es decir, se puede garantizar sin ningún medio, que generalmente se llama el principio antes. Si la orden de ejecución de dos operaciones no puede derivarse del principio antes, entonces no pueden garantizar que su orden y las máquinas virtuales puedan reordenarlas a voluntad.
Presentemos el Principio de Before (Principio de ocurrencia de prioridad):
Estos 8 principios están extraídos de la "comprensión profunda de las máquinas virtuales Java".
Entre estas 8 reglas, las primeras 4 reglas son más importantes, mientras que las últimas 4 reglas son obvias.
Expliquemos las primeras 4 reglas a continuación:
Para las reglas del orden del programa, tengo entendido que la ejecución de un código de programa parece ordenarse en un solo hilo. Tenga en cuenta que aunque esta regla menciona que "la operación escrita en el frente ocurre primero en la operación escrita en la parte posterior", este debería ser el orden en el que el programa parece ejecutarse en la secuencia de código, porque la máquina virtual puede reordenar el código del programa instruido. Aunque se realiza la reordenamiento, el resultado de la ejecución final es consistente con la ejecución secuencial del programa, y solo reordenará instrucciones que no tienen dependencias de datos. Por lo tanto, en un solo hilo, la ejecución del programa parece ejecutarse de manera ordenada, que debe entenderse con cuidado. De hecho, esta regla se utiliza para garantizar la corrección de los resultados de ejecución del programa en un solo hilo, pero no puede garantizar la corrección del programa de manera múltiple.
La segunda regla también es más fácil de entender, es decir, si el mismo bloqueo está en un estado bloqueado, debe liberarse antes de que se pueda continuar la operación de bloqueo.
La tercera regla es una regla relativamente importante y también es lo que se discutirá más adelante. Intuitivamente, si un hilo escribe primero una variable y luego se lee un hilo, entonces la operación de escritura definitivamente ocurrirá primero en la operación de lectura.
La cuarta regla en realidad refleja que el principio es transitivo.
4. Análisis en profundidad de palabras clave volátiles
He hablado de muchas cosas antes, pero en realidad están allanando la forma de decir la palabra clave volátil, así que vamos al tema.
1. Semántica de dos capas de palabras clave volátiles
Una vez que Volatile modifica una variable compartida (variables de miembro de la clase, variables de miembros estáticos), tiene dos capas de semántica:
1) Asegúrese de que la visibilidad de diferentes subprocesos al operar esta variable, es decir, un hilo modifica el valor de una determinada variable, y este nuevo valor es inmediatamente visible para otros hilos.
2) Está prohibido reordenar las instrucciones.
Veamos primero un código de código. Si el hilo 1 se ejecuta primero y el hilo 2 se ejecuta más tarde:
// hilo 1 boolean stop = false; while (! stop) {dosomthething ();} // hilo 2stop = true;Este código es una pieza de código muy típica, y muchas personas pueden usar este método de marcado al interrumpir los hilos. Pero de hecho, ¿se ejecutará este código completamente correctamente? ¿Se interrumpirá el hilo? No necesariamente. Quizás la mayoría de las veces, este código puede interrumpir los hilos, pero también puede hacer que el hilo no se interrumpa (aunque esta posibilidad es muy pequeña, una vez que esto suceda, causará un bucle muerto).
Explicemos por qué este código puede hacer que el hilo no interrumpa. Como se explicó anteriormente, cada hilo 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 parada y la pondrá 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 va a hacer otras cosas, entonces Thread 1 no sabe sobre los cambios de Thread 2 a la variable de parada, por lo que continuará en bucle.
Pero después de modificar con volátil, se vuelve diferente:
Primero: el uso de la palabra clave volátil forzará el valor modificado que se escriba a la memoria principal de inmediato;
Segundo: si usa la palabra clave volátil, cuando el subproceso 2 la modifica, la línea de caché de la variable de caché se detendrá en la memoria de trabajo de Thread 1 no será válida (si se refleja en la capa de hardware, la línea de caché correspondiente en la caché L1 o L2 de la CPU no es válida);
Tercero: dado que la línea de caché de la variable de caché se detiene en la memoria de trabajo de Thread 1 no es válida, Thread 1 la leerá en la memoria principal cuando lea el valor de la variable parada nuevamente.
Luego, cuando Thread 2 modifica el valor de parada (por supuesto, hay 2 operaciones aquí, modificando el valor en la memoria de trabajo de Thread 2 y luego escribir el valor modificado a la memoria), la línea de caché de la variable de caché se detendrá en la memoria de trabajo de Thread 1 no será válida. Cuando el hilo 1 se lee, encuentra que su línea de caché no es válida. Esperará a que se actualice la dirección de memoria principal correspondiente de la línea de caché, y luego lea el último valor en la memoria principal correspondiente.
Entonces lo que lee el hilo 1 es el último valor correcto.
2. ¿Volátil garantiza la atomicidad?
De lo anterior, sabemos que la palabra clave volátil garantiza la visibilidad de las operaciones, pero ¿puede volátiles garantizar que las operaciones de las variables sean atómicas?
Veamos un ejemplo a continuación:
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); }}¿Piensa en cuál es el resultado de la salida de este programa? Tal vez algunos amigos piensan que son 10,000. Pero de hecho, ejecutarlo encontrará que los resultados de cada ejecución son inconsistentes, y es un número de menos de 10,000.
Algunos amigos pueden tener preguntas, está mal. Lo anterior es realizar una operación de autoincremento en la variable inc. Dado que Volátil garantiza la visibilidad, después del auto-incremento de Inc en cada hilo, el valor modificado se puede ver en otros hilos. Por lo tanto, 10 hilos han realizado 1000 operaciones respectivamente, por lo que el valor final de Inc debe ser 1000*10 = 10000.
Hay un malentendido aquí. La palabra clave volátil puede garantizar la visibilidad, pero el programa anterior es incorrecto porque no puede garantizar la atomicidad. La visibilidad solo puede garantizar que el último valor se lea cada vez, pero Volatile no puede garantizar la atomicidad de la operación de las variables.
Como se mencionó anteriormente, la operación de incremento automático no es atómica. Incluye leer el valor original de una variable, realizar una operación adicional y escribir en la memoria de trabajo. Es decir, las tres suboperaciones de la operación de autoincremento pueden realizarse por separado, lo que puede conducir a la siguiente situación:
Si el valor de Variable Inc en un momento determinado es 10,
Thread 1 realiza una operación de autoincremento en la variable. El hilo 1 primero lee el valor original de la variable Inc, y luego el hilo 1 está bloqueado;
Luego, Thread 2 realiza una operación de autoincremento en la variable, y Thread 2 también lee el valor original de la variable inc. Dado que el subproceso 1 solo realiza una operación de lectura en la variable Inc y no modifica la variable, no causará que la línea de caché de la variable de caché Inc Cache Inc en el subproceso 2 no sea válida. Por lo tanto, el hilo 2 irá directamente a la memoria principal para leer el valor de Inc. Cuando se encuentra que el valor de Inc es 10, realiza una operación de agregar 1, y escribe 11 a la memoria de trabajo, y finalmente lo escribe a la memoria principal.
Luego, el hilo 1 realiza la operación de adición. Dado que el valor de INC se ha leído, tenga en cuenta que el valor de Inc en el subproceso 1 sigue siendo 10 en este momento, por lo que después de que el hilo 1 agrega Inc, el valor de Inc es 11, luego escribe 11 para trabajar la memoria y finalmente lo escribe a la memoria principal.
Luego, después de que los dos hilos realicen una operación de autoincremento, Inc solo aumenta en 1.
Habiendo explicado esto, algunos amigos pueden tener preguntas, está mal. ¿No se garantiza que una variable invalidará la línea de caché al modificar la variable volátil? Luego, otros hilos leerán el nuevo valor. Sí, esto es correcto. Esta es la regla de la variable volátil en la regla de Hogar Borefore anterior, pero debe tenerse en cuenta que si el subproceso 1 lee la variable y se bloquea, el valor de Inc no se modificará. Luego, aunque Volatile puede garantizar que el subproceso 2 lea el valor de Variable Inc de la memoria, Thread 1 no lo ha modificado, por lo que Thread 2 no verá el valor modificado en absoluto.
La causa raíz es que la operación de autoincremento no es una operación atómica, y el volátil no puede garantizar que ninguna operación en variables sea atómica.
Cambiar el código anterior a cualquiera de los siguientes puede lograr el efecto:
Use sincronizado:
Prueba de clase pública {public int Inc = 0; public sincronized 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); }} Usando el bloqueo:
Prueba de clase pública {public int Inc = 0; Bloqueo de bloqueo = nuevo reentrantlock (); public void aumento () {Lock.lock (); intente {inc ++; } Finalmente {Lock.unlock (); }} 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 sido ejecutados thread.yield (); System.out.println (test.inc); }} Usando AtomicInteger:
Prueba de clase pública {public atomicInteger inc = new AtomicInteger (); public void aumento () {Inc.getAndIrrement (); } 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 sido ejecutados thread.yield (); System.out.println (test.inc); }}Algunas clases de operación atómica se proporcionan bajo Java.util.concurrent.Atomic Package of Java 1.5, a saber, el auto-incremento (Operación Agregar 1), la autodescrata (Operación Agregar 1), la operación de suma (Agregar un número) y la operación de subtracción (Agregar un número) de los tipos de datos básicos para garantizar que estas operaciones sean operaciones atómicas. Atomic usa CAS para implementar operaciones atómicas (comparar e intercambiar). CAS realmente se implementa utilizando las instrucciones CMPXCHG proporcionadas por el procesador, y el procesador ejecuta las instrucciones CMPXCHG es una operación atómica.
3. ¿Puede garantizar el orden?
Como se mencionó anteriormente, la palabra clave volátil puede prohibir el reordenamiento de instrucciones, por lo que volátil puede garantizar el orden en cierta medida.
Hay dos significados reordenados de palabras clave volátiles:
1) Cuando el programa ejecuta una operación de lectura o escritura de la variable volátil, todos los cambios en las operaciones anteriores deben haberse realizado, y el resultado ya está visible para las operaciones posteriores; Las operaciones posteriores aún no deben haberse realizado;
2) Al realizar la optimización de la instrucción, la declaración accedida a la variable volátil no se puede colocar detrás de ella, ni las declaraciones que siguen a la variable volátil se colocarán ante él.
Tal vez lo que se dice anteriormente es un poco confuso, así que da un ejemplo simple:
// x e y son variables no volátiles // El indicador es una variable volátil x = 2; // Declaración 1y = 0; // Declaración 2flag = True; // Declaración 3x = 4; // Declaración 4y = -1; // Declaración 5
Dado que la variable de indicador es una variable volátil, al realizar el proceso de reordenamiento de instrucciones, la Declaración 3 no se colocará antes de la Declaración 1 y 2, ni se colocará después de la Declaración 3, y la Declaración 4 y 5. Sin embargo, no está garantizado que el orden de la Declaración 1 y la Declaración 2 y el orden de la declaración 4 y la Declaración 5 no estén garantizados.
Además, la palabra clave volátil puede garantizar que cuando se ejecute la declaración 3, la declaración 1 y la declaración 2 deben ejecutarse, y los resultados de ejecución de la Declaración 1 y la declaración 2 sean visibles a la Declaración 3, Declaración 4 y Declaración 5.
Así que volvamos al ejemplo anterior:
// hilo 1: context = loadContext (); // estado 1inited = true; // estado 2 // hilo 2: while (! Inited) {sleep ()} dosomethingwithConfig (contexto);Cuando di este ejemplo, mencioné que es posible que la declaración 2 se ejecute antes de la Declaración 1, tanto tiempo que puede hacer que el contexto no se inicialice, y Thread 2 usa el contexto no inicializado para operar, lo que resulta en un error de programa.
Si la variable initado se modifica con la palabra clave volátil, este problema no ocurrirá, porque cuando se ejecuta la declaración 2, definitivamente asegurará que el contexto se haya inicializado.
4. El principio y el mecanismo de implementación del volátil
La descripción anterior de algunos usos de la palabra clave volátil se originó. Let’s discuss how volatile ensures visibility and prohibits instructions to reorder.
下面这段话摘自《深入理解Java虚拟机》:
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
五.使用volatile关键字的场景
synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:
1)对变量的写操作不依赖于当前值
2)该变量没有包含在具有其他变量的不变式中
实际上,这些条件表明,可以被写入volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。
事实上,我的理解就是上面的2个条件需要保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。
下面列举几个Java中使用volatile的几个场景。
1.状态标记量
volatile boolean flag = false; while(!flag){ doSomething();} public void setFlag() { flag = true;} volatile boolean inited = false;//线程1:context = loadContext(); inited = true; //线程2:while(!inited ){sleep()}doSomethingwithconfig(context);2.double check
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(); }} instancia de retorno; }}Referencias:
《Java编程思想》
《深入理解Java虚拟机》
Lo anterior es todo el contenido de este artículo. Espero que sea útil para el aprendizaje de todos y espero que todos apoyen más a Wulin.com.