La variable volátil en el lenguaje Java puede considerarse como un "sincronizado más ligero"; En comparación con el bloque sincronizado, la variable volátil requiere menos codificación de codificación y tiempo de ejecución, pero la funcionalidad que puede lograr es solo una parte de sincronizado.
Cerrar
Los bloqueos proporcionan dos características principales: exclusión mutua y visibilidad.
Variables volátiles
La variable volátil tiene las características de visibilidad de sincronizado, pero no tiene las características atómicas. Esto significa que el hilo puede descubrir automáticamente el último valor de la variable volátil.
Las variables volátiles se pueden usar para proporcionar seguridad de subprocesos, pero solo se pueden aplicar a un conjunto muy limitado de casos de uso: no existe una restricción entre múltiples variables o entre el valor actual de una variable y el valor modificado. Por lo tanto, usar volátil solo no es suficiente para implementar contadores, mutexes o cualquier clase con invariantes asociados con múltiples variables (por ejemplo, "inicio <= end").
Por simplicidad o escalabilidad, puede tender a usar variables volátiles en lugar de cerraduras. Algunos modismos son más fáciles de codificar y leer cuando se usan variables volátiles en lugar de cerraduras. Además, la variable volátil no causa bloqueo de roscas como un bloqueo y, por lo tanto, rara vez causa problemas de escalabilidad. En algunos casos, la variable volátil también puede proporcionar ventajas de rendimiento sobre los bloqueos si la operación de lectura es mucho más grande que la operación de escritura.
Condiciones para el uso correcto de variables volátiles
Solo puede usar la variable volátil en lugar de bloqueos en casos limitados. Para hacer la seguridad de los subprocesos ideales de la variable volátil, las siguientes dos condiciones deben cumplirse al mismo tiempo:
Las operaciones de escritura a las variables no dependen del valor actual.
Esta variable no está incluida en el invariante con otras variables.
De hecho, estas condiciones indican que estos valores válidos que se pueden escribir en la variable volátil son independientes del estado de cualquier programa, incluido el estado actual de la variable.
Los límites de la primera condición evitan que la variable volátil se use como un contador seguro de hilo. Aunque una operación incremental (x ++) parece una operación separada, en realidad es una operación combinada compuesta de una secuencia de operaciones de lectura-modificación-escritura que debe realizarse atómicamente, y el volátil no puede proporcionar las propiedades atómicas necesarias. La implementación de la operación correcta requiere mantener constante el valor de x durante la operación, lo que no es posible con la variable volátil. (Sin embargo, si el valor se ajusta para que se escriba solo desde un solo hilo, la primera condición se puede ignorar).
La mayoría de las situaciones de programación entran en conflicto con una de estas dos condiciones, lo que hace que la variable volátil no sea tan universalmente aplicable a la seguridad de los subprocesos como se sincronizada. El listado 1 muestra una clase de rango numérico no seguro de huella. Contiene un invariante: el límite inferior siempre es menor o igual al límite superior.
Dar un ejemplo
Veamos un ejemplo a continuación. Implementamos un contador. Cada vez que se inicia el hilo, se llamará al método contador Inc para agregar el contador al entorno de ejecución - JDK Versión: JDK1.6.0_31, Memoria: CPU 3G: X86 2.4G
Contador de clase pública {public static int count = 0; public static void Inc () {// El retraso aquí es 1 milisegundo, lo que hace que el resultado sea obvio try {Thread.sleep (1); } capt (interruptedException e) {} count ++; } public static void main (string [] args) {// Inicie 1000 hilos al mismo tiempo para realizar cálculos I ++ y vea el resultado real para (int i = 0; i <1000; i ++) {new Thread (new Runnable () {@Override public void run () {contador.inc ();}}). Start (); } // El valor de cada ejecución aquí puede ser diferente, tal vez 1000 System.out.println ("Ejecutar resultado: contador.count =" + contador.count); }} Resultado de ejecución: contador.count = 995
El resultado de la operación real puede ser diferente cada vez. El resultado de la máquina es: Running Result: Count.Count = 995. Se puede ver que en un entorno múltiple, el recuento no espera que el resultado sea 1000.
Muchas personas piensan que este es un problema de concurrencia multiproceso. Solo necesita agregar volátil antes del recuento de variables para evitar este problema. Luego estamos modificando el código para ver si el resultado cumple con nuestras expectativas.
Contador de clase pública {public volatile static int count = 0; public static void Inc () {// El retraso aquí es 1 milisegundo, lo que hace que el resultado sea obvio try {Thread.sleep (1); } capt (interruptedException e) {} count ++; } public static void main (string [] args) {// Inicie 1000 hilos al mismo tiempo, realice cálculos I ++ y vea el resultado real para (int i = 0; i <1000; i ++) {new Thread (new Runnable () {@Override public void run () {contunder.inc ();}). Start (); } // El valor de cada ejecución aquí puede ser diferente, posiblemente 1000 System.out.println ("Ejecutar resultado: contador.count =" + contador.count); }}Resultado en ejecución: contador.count = 992
El resultado de la operación aún no es tan 1000 como esperábamos. Analicemos las razones a continuación
En el artículo de Java Garbage Collection, se describe la asignación de la memoria en el momento de JVM. Una de las áreas de memoria es la pila de máquina virtual JVM. Cada hilo tiene una pila de subprocesos cuando se ejecuta, y la pila de subprocesos guarda la información del valor variable durante las ejecuciones de subproceso. Cuando un subproceso accede al valor de un cierto objeto, primero encuentra el valor de la variable correspondiente a la memoria del montón a través de la referencia del objeto, y luego carga el valor específico de la variable de memoria de los pisos en la memoria local del hilo para crear una copia variable. Después de eso, el subproceso ya no tiene ninguna relación con el valor de la variable de memoria Heap del objeto, pero modifica directamente el valor de la variable de copia, y en un momento determinado después de la modificación (antes de que salga el subproceso), escribe automáticamente el valor de la variable de subproceso de nuevo a la variable de montón del objeto. De esta manera, el valor del objeto en el montón cambiará. La siguiente imagen describe la interacción de este escrito
EAD y carga variables de copia desde la memoria principal a la memoria de trabajo actual
Use y asigne el código de ejecución para cambiar el valor de la variable compartida
Almacene y escriba Actualizar contenido relacionado con la memoria principal con datos de memoria de trabajo
donde el uso y la asignación puede aparecer varias veces
Sin embargo, estas operaciones no son atómicas, es decir, después de la carga de lectura, si la variable de recuento de memoria principal se modifica, el valor en la memoria de trabajo de subproceso no causará cambios correspondientes desde que se ha cargado, por lo que el resultado calculado será diferente de la esperada.
Para las variables modificadas por Volatile, la máquina virtual JVM solo garantiza que el valor cargado de la memoria principal a la memoria de trabajo de subprocesos sea la última
Por ejemplo, si el hilo 1 y el hilo 2 realizan operaciones de lectura y carga, y encuentran que el valor del recuento en la memoria principal es 5, entonces el último valor se cargará
Después de que el recuento de montón se modifique en el subproceso 1, se escribirá en la memoria principal, y la variable de recuento en la memoria principal se convertirá en 6.
Dado que Thread 2 ya ha realizado la operación de lectura y carga, el valor variable del recuento de memoria principal también se actualizará a 6 después de la operación.
Esto hace que ocurra la concurrencia después de que dos hilos se modifican con la palabra clave volátil en el tiempo.