Prefacio
A veces, si usa la sincronización solo para leer y escribir uno o dos campos de instancia, parece demasiado costoso. La palabra clave volátil proporciona un mecanismo sin bloqueo para el acceso sincrónico a los campos de instancia. Si un dominio se declara como volátil, el compilador y la máquina virtual saben que el dominio puede ser actualizado simultáneamente por otro hilo. Antes de hablar sobre la palabra clave volátil, necesitamos comprender los conceptos relevantes del modelo de memoria y las tres características en la programación concurrente: atomicidad, visibilidad y orden.
1. Modelo de memoria Java con atomicidad, visibilidad y orden
El modelo de memoria Java estipula que todas las variables existen en la memoria principal, y cada hilo tiene su propia memoria de trabajo. 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.
En Java, ejecute la siguiente declaración:
int i = 3;
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 3 directamente en la memoria principal.
Entonces, ¿qué garantías proporciona el lenguaje Java para la atomicidad, la visibilidad y el orden?
Atomicidad
Las operaciones de lectura y asignación de variables del tipo de datos básicos son operaciones atómicas, es decir, estas operaciones no pueden interrumpirse y ejecutar o no.
Echemos un vistazo al siguiente código:
x = 10; // Declaración 1y = x; // Declaración 2x ++; // Declaración 3x = x + 1; // Declaración 4
La única declaración 1 es una operación atómica, y ninguna de las otras tres declaraciones son operaciones atómicas.
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.
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.
Hay muchas clases en el paquete java.util.concurrent.atomic que utilizan instrucciones de nivel de máquina muy eficientes (en lugar de bloqueos) para garantizar la atomicidad de otras operaciones. Por ejemplo, la clase AtomicInteger proporciona métodos incrementados y disminuciones, que aumentan y disminuyen respectivamente un entero de manera atómica. La clase AtomicInteger se puede usar de manera segura como un contador compartido sin sincronización.
Además, este paquete también contiene clases atómicas como Atomicboolean, Atomiclong y Atomicreference para programadores de sistemas que desarrollan solo herramientas concurrentes, y los programadores de aplicaciones no deben usar estas clases.
Visibilidad
La visibilidad se refiere a la visibilidad entre los hilos, y el estado modificado de un hilo es visible para otro hilo. Ese es el resultado de una modificación de hilo. Otro hilo se verá de inmediato.
Cuando Volatile modifica una variable compartida, asegura que el valor modificado se actualice a la memoria principal de inmediato, por lo que es visible a otros subprocesos. Cuando otros hilos necesitan leer, 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.
Ordenado
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.
La palabra clave volátil se puede usar para garantizar una cierta "línea de orden". 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.
2. 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:
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; 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).
¿Por qué es posible causar falla de hilo para interrumpir? Cada hilo tiene su propia memoria de trabajo durante su proceso de ejecución. Cuando el subproceso 1 se está ejecutando, copiará el valor de la variable de parada y lo 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:
¿Volátil garantiza la atomicidad?
Sabemos que la palabra clave volátil garantiza la visibilidad de las operaciones, pero ¿puede garantizar volátiles garantizar que las operaciones de las variables sean atómicas?
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(); } // Asegúrese de que los subprocesos anteriores hayan completado la ejecución mientras (Thread.ActiveCount ()> 1) Thread.yield (); System.out.println (test.inc); }} El resultado de este código es inconsistente cada vez que se ejecuta. Es un número menos de 10,000. 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 1 adicional y escribir en la memoria de trabajo. Es decir, las tres suboperaciones de la operación de auto-incremento pueden ejecutarse por separado.
Si el valor de Variable Inc es 10 en un momento determinado, el hilo 1 realiza la operación de autoincremento en la variable, el hilo 1 primero lee el valor original de Variable Inc, y luego se bloquea el hilo 1; Luego, Thread 2 realiza una operación de autoincremento en la variable, y Thread 2 también lee el valor original de Variable Inc. Dado que Thread 1 solo realiza una operación de lectura en la variable Inc y no modifica la variable, no causará la línea de caché de la variable de caché Inc en el subproceso 2 no es válido en la memoria de trabajo, por lo que Thread 2 irá directamente a la memoria principal para leer el valor de Inc. Cuando se encuentra que el valor de Inc es 10, realiza un aumento de 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.
La operación de autoincremento no es una operación atómica, y el volátil no puede garantizar que cualquier operación en una variable sea atómica.
¿Puede volátil 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:
3. Use la palabra clave volátil correctamente
La palabra clave sincronizada evita que múltiples subprocesos ejecute un código de código al mismo tiempo, lo que afectará en gran medida la eficiencia de ejecución del programa. El rendimiento de la palabra clave volátil es mejor que sincronizar en algunos casos. Sin embargo, debe tenerse 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 la operación. En términos generales, las siguientes dos condiciones deben cumplirse cuando se usan volátiles:
La primera condición es que no se puede ser operaciones como el autoinforme y el desacuerdo. Como se mencionó anteriormente, Volátil no garantiza la atomicidad.
Demos un ejemplo de esto. Contiene un invariante: el límite inferior siempre es menor o igual al límite superior.
Public Class NumberRange {Private Volatile int Lower, superior; public int getLower () {return Lower; } public int getUpper () {return superior; } public void setLower (int value) {if (valor> superior) tirar nueva ilegalargumentException (...); menor = valor; } public void setUpper (int value) {if (valor <menor) tirar nueva ilegalargumentException (...); superior = valor; }} De esta manera, limita las variables de estado alcanzadas, por lo que definir los campos inferiores y superiores como tipos volátiles no implementa completamente la seguridad del hilo de la clase y, por lo tanto, aún requiere sincronización. De lo contrario, si dos hilos ejecutan setLower y setUpper con valores inconsistentes al mismo tiempo, el rango será inconsistente. Por ejemplo, si el estado inicial es (0, 5), al mismo tiempo, en el hilo A las llamadas SetLower (4) y las llamadas de hilo B setUpper (3), es obvio que los valores almacenados en el cruce almacenado por estas dos operaciones no cumplen con las condiciones, entonces ambos hilos pasarán el cheque para proteger el invariante, de modo que el último valor de rango es (4, 3), lo que está obviamente equivocado.
De hecho, es para garantizar la atomicidad de la operación para usar volátiles. Hay dos escenarios principales para usar volátiles:
Banderas de estado
Volátil boolean shutdownRequested; ... public void shutdown () {shutdownRequested = true; } public void dowork () {while (! shutdownRequested) {// hacer cosas}}Es probable que el método apagado () se llame desde fuera del bucle, es decir, en otro hilo, por lo que se debe realizar algún tipo de sincronización para garantizar que la visibilidad de la variable de cierre de cierre se implementa correctamente. Sin embargo, escribir un bucle con bloques sincronizados es mucho más problemático que escribir con banderas de estado volátiles. Debido a que volátiles simplifica la codificación y los indicadores de estado no dependen de ningún otro estado del programa, es muy adecuado para volátil aquí.
Modo de verificación doble (DCL)
clase pública Singleton {private volátil static singleton instancia = null; public static singleton getInstance () {if (instance == null) {sincronizado (this) {if (instance == null) {instancia = nuevo singleton (); }} instancia de retorno; }} Usar volátiles aquí afectará más o menos el rendimiento, pero teniendo en cuenta la corrección del programa, todavía vale la pena sacrificar este rendimiento.
La ventaja de DCL es que tiene una alta utilización de recursos. Los objetos singleton se instancian solo cuando la getInstance se ejecuta por primera vez, lo cual es altamente eficiente. La desventaja es que la reacción es ligeramente más lenta cuando se carga por primera vez, y hay ciertos defectos en entornos de alta concurrencia, aunque la probabilidad de ocurrencia es muy pequeña.
Aunque DCL resuelve los problemas de consumo de recursos, sincronización innecesaria, seguridad de los hilos, etc. Hasta cierto punto, todavía tiene problemas de falla en algunos casos, es decir, falla de DCL. En el libro "Práctica de programación de concurrencia Java", recomienda usar el siguiente código (patrón de clase interna estática) para reemplazar DCL:
public class Singleton {private singleton () {} public static singleton getInstance () {return singletonholder.sinstance; } clase privada de clase estática Singletonholder {Private static final Singleton sinstance = new Singleton (); }} Sobre cheques dobles, puede ver
4. Resumen
En comparación con las cerraduras, la variable volátil es un mecanismo de sincronización muy simple pero al mismo tiempo muy frágil que en algunos casos proporcionará un mejor rendimiento y escalabilidad que las cerraduras. Si sigue estrictamente las condiciones de uso de volátiles que son realmente independientes de otras variables y sus propios valores anteriores, puede usar volátiles en lugar de sincronizar para simplificar el código en algunos casos. Sin embargo, el código que usa volátil es a menudo más propenso a los errores que el código que usa bloqueos. Este artículo presenta dos casos de uso más comunes en los que se puede usar volátiles en lugar de sincronizarse. En otros casos, es mejor que usemos sincronizado.
Lo anterior se trata de este artículo, espero que sea útil para el aprendizaje de todos.