Serie de programación concurrente de Java [inacabado]:
• Programación de concurrencia de Java: teoría del núcleo
• Programación concurrente de Java: sincronizado y sus principios de implementación
• Programación concurrente de Java: optimización subyacente sincronizada (bloqueo liviano, bloqueo sesgado)
• Programación concurrente de Java: colaboración entre hilos (espera/notificar/dormir/rendimiento/unirse)
• Programación concurrente de Java: el uso de volátiles y sus principios
1. El papel de volátil
En el artículo "Programación de concurrencia de Java: teoría del núcleo", hemos mencionado los problemas de visibilidad, orden y atomicidad. Por lo general, podemos resolver estos problemas a través de la palabra clave sincronizada. Sin embargo, si comprende el principio de sincronizado, debe saber que Sincronized es una operación de peso relativamente pesado y tiene un impacto relativamente grande en el rendimiento del sistema. Por lo tanto, si hay otras soluciones, generalmente evitamos usar sincronizado para resolver el problema. La palabra clave volátil es otra solución proporcionada en Java para resolver los problemas de visibilidad y orden. Con respecto a la atomicidad, también es un punto que todos son propensos al malentendido: una sola operación de lectura/escritura de variables volátiles puede garantizar la atomicidad, como las variables de tipo largo y doble, pero no puede garantizar la atomicidad de las operaciones I ++, porque en esencia, i ++ es operaciones de lectura y escritura dos veces.
2. Uso de volátil
Con respecto al uso de volátiles, podemos usar varios ejemplos para ilustrar su uso y escenarios.
1. Evite el reordenamiento
Analicemos el problema de reordenamiento de uno de los ejemplos más clásicos. Todos deben estar familiarizados con la implementación del modelo Singleton, y en un entorno concurrente, generalmente podemos usar el método de bloqueo de doble verificación (DCL) para implementarlo. El código fuente es el siguiente:
paquete com.paddx.test.concurrent; public class Singleton {public static volatile singleton singleton; / *** El constructor es privado, prohibiendo instanciación externa*/ private singleton () {}; public static singleton getInstance () {if (singleton == null) {sincronizado (singleton) {if (singleton == null) {singleton = new Singleton (); }} return Singleton; }}Ahora analicemos por qué necesitamos agregar la palabra clave volátil entre el singleton variable. Para comprender este problema, primero debe comprender el proceso de construcción de objetos. La instancia de un objeto se puede dividir en tres pasos:
(1) Asignar espacio de memoria.
(2) Inicializar el objeto.
(3) Asigne la dirección del espacio de memoria a la referencia correspondiente.
Sin embargo, dado que el sistema operativo puede reordenar las instrucciones, el proceso anterior también puede convertirse en el siguiente proceso:
(1) Asignar espacio de memoria.
(2) Asigne la dirección del espacio de memoria a la referencia correspondiente.
(3) Inicializar el objeto
Si este proceso es el proceso, una referencia de objeto no inicializada puede estar expuesta en un entorno multi-subprocesos, lo que resulta en resultados impredecibles. Por lo tanto, para evitar el reordenamiento de este proceso, necesitamos establecer la variable en una variable de tipo volátil.
2. Lograr visibilidad
El problema de visibilidad se refiere principalmente a un hilo que modifica el valor de la variable compartida, mientras que el otro hilo no puede verlo. La razón principal del problema de visibilidad es que cada hilo tiene su propio área de caché: memoria de trabajo de subprocesos. La palabra clave volátil puede resolver efectivamente este problema. Veamos los siguientes ejemplos para conocer su función:
paquete com.paddx.test.concurrent; public class volatiletest {int a = 1; int b = 2; Cambio publicitario public () {a = 3; b = a; } public void print () {System.out.println ("b ="+b+"; a ="+a); } public static void main (string [] args) {while (true) {final volatiletest test = new Volatiletest (); new Thread (new Runnable () {@Override public void run () {try {horth.sleep (10);} capt (interruptedException e) {e.printstacktrace ();} test.change ();}}). start (); new Thread (new Runnable () {@Override public void run () {try {Thread.sleep (10);} Catch (InterruptedException e) {E.PrintStackTrace ();} test.print ();}}). Start (); }}}Hablando intuitivamente, solo hay dos resultados posibles para este código: b = 3; a = 3 o b = 2; a = 1. Sin embargo, ejecutar el código anterior (tal vez lleva un poco más de tiempo), encontrará que, además de los dos resultados anteriores, también hay un tercer resultado:
...... b = 2; a = 1b = 2; a = 1b = 3; a = 3b = 3; a = 3b = 3; a = 1b = 3; a = 3b = 2; a = 1b = 3; a = 3b = 3; a = 3b = 3; a = 3 ...
¿Por qué aparece un resultado como b = 3; a = 1? En circunstancias normales, si ejecuta primero el método de cambio y luego ejecuta el método de impresión, el resultado de salida debe ser b = 3; a = 3. Por el contrario, si ejecuta el método de impresión primero y luego ejecuta el método de cambio, el resultado debe ser b = 2; a = 1. Entonces, ¿cómo sale el resultado de b = 3; a = 1? La razón es que el primer hilo modifica el valor A = 3, pero es invisible para el segundo hilo, por lo que se produce este resultado. Si tanto A como B se cambian a variables de tipo volátil y se ejecutan, el resultado de B = 3; A = 1 nunca aparecerá nuevamente.
3. Asegúrese de atomicidad
El tema de la atomicidad se ha explicado anteriormente. Volátil solo puede garantizar la atomicidad para lectura/escritura individuales. Este problema se puede describir en JLS:
17.7 El tratamiento no atómico del doble y largo para los fines del modelo de memoria del lenguaje de programación Java, una sola escritura a un valor largo o doble no volátil se trata como dos escrituras separadas: una a cada mitad de 32 bits. Esto puede resultar en una situación en la que un hilo ve los primeros 32 bits de un valor de 64 bits de una escritura, y los segundos 32 bits de otra escritura. Write y lecturas de valores volátiles largos y dobles siempre son atómicos. Las escrituras y lecturas de referencias son siempre atómicas, independientemente de si se implementan como valores de 32 bits o 64 bits. Algunas implementaciones pueden encontrar conveniente dividir una acción de escritura única en un valor de 64 bits de largo o doble en dos acciones de escritura en valores adyacentes de 32 bits. Por el bien de la eficiencia, este comportamiento es específico de la implementación; Una implementación de la máquina virtual Java es libre de realizar escrituras a valores largos y dobles atómicamente o en dos partes. Se alienta a las implementaciones de la máquina virtual Java a evitar dividir los valores de 64 bits cuando sea posible. Se alienta a los programadores a declarar valores compartidos de 64 bits como volátiles o sincronizar sus programas correctamente para evitar posibles compensaciones.
El contenido de este pasaje es más o menos similar a lo que describí anteriormente. Debido a que las operaciones de los dos tipos de datos de larga y doble se pueden dividir en dos partes: 32 bits altos y 32 bits bajos, los tipos largos o dobles ordinarios pueden no ser atómicos. Por lo tanto, se alienta a todos a establecer las variables largas y dobles compartidas en tipos volátiles, lo que puede garantizar que las operaciones de lectura/escritura únicas de larga y doble sean atómicas en cualquier caso.
Hay un problema de que las variables volátiles garantizan la atomicidad, que se malinterpreta fácilmente. Ahora demostraremos este problema a través del siguiente programa:
paquete com.paddx.test.concurrent; public class Volatiletest01 {Volatile int i; public void addi () {i ++; } public static void main (string [] args) lanza interruptedException {final volatiletest01 test01 = new Volatiletest01 (); for (int n = 0; n <1000; n ++) {new Thread (new Runnable () {@Override public void run () {try {Thread.sleep (10);} Catch (InterruptedException e) {E.PrintStackTrace ();} test01.addi ();}). Start ();; } Thread.sleep (10000); // Espere 10 segundos para garantizar que la ejecución del programa anterior se complete System.out.println (test01.i); }}Puede creer erróneamente que después de agregar la palabra clave volátil a la variable I, este programa es seguro de hilo. Puede intentar ejecutar el programa anterior. Aquí están los resultados de mi carrera local:
Tal vez todos ejecutan los resultados de manera diferente. Sin embargo, debe verse que el volátil no puede garantizar la atomicidad (de lo contrario, el resultado debería ser 1000). La razón también es muy simple. I ++ es en realidad una operación compuesta, que incluye tres pasos:
(1) Lea el valor de i.
(2) Agregue 1 a i.
(3) Escriba el valor de I Back a Memory.
No hay garantía de que estas tres operaciones sean atómicas. Podemos garantizar la atomicidad de las operaciones +1 a través de AtomicInteger o sincronizado.
Nota: El método Thread.sleep () se ejecutó en muchos lugares en las secciones de código anteriores, con el propósito de aumentar la posibilidad de problemas de concurrencia y no tiene otro efecto.
3. El principio de volátil
A través de los ejemplos anteriores, básicamente debemos saber qué es volátil y cómo usarlo. Ahora echemos un vistazo a cómo se implementa la capa subyacente de volátil.
1. Implementación de visibilidad:
Como se mencionó en el artículo anterior, el hilo en sí no interactúa directamente con los datos de la memoria principal, sino que completa las operaciones correspondientes a través de la memoria de trabajo del hilo. Esta es también la razón esencial por la cual los datos entre hilos son invisibles. Por lo tanto, para lograr la visibilidad de las variables volátiles, puede comenzar directamente desde este aspecto. Hay dos diferencias principales entre las operaciones de escritura en variables volátiles y variables ordinarias:
(1) Al modificar la variable volátil, el valor modificado se verá obligado a actualizar la memoria principal.
(2) La modificación de la variable volátil hará que los valores de la variable correspondientes en la memoria de trabajo de otros subprocesos fallen. Por lo tanto, al leer el valor de esta variable nuevamente, debe volver a leer el valor en la memoria principal.
A través de estas dos operaciones, se puede resolver el problema de visibilidad de las variables volátiles.
2. Implementación ordenada:
Antes de explicar este problema, primero entendamos las reglas de sucesión antes de Java. La definición de sucesión antes en JSR 133 es la siguiente:
Se pueden ordenar dos acciones mediante una relación antes. Si una acción ocurre antes que otra, entonces la primera es visible y ordenada antes del segundo.
En términos de laicos, si se produce antes de B, cualquier operación A es visible para b. (Todos deben recordar esto, porque la palabra suceder antes se malinterpreta fácilmente como antes y después del tiempo). Echemos un vistazo a lo que las reglas de sucesión se definen en JSR 133:
• Cada acción en un hilo ocurre antes de cada acción posterior en ese hilo. • Un desbloqueo en un monitor ocurre antes de cada bloqueo posterior en ese monitor. • Una escritura en un campo volátil ocurre antes de cada lectura posterior de ese volátil. • Una llamada para iniciar () en un hilo ocurre antes de cualquier acción en el hilo iniciado. • Todas las acciones en un hilo ocurren antes de que cualquier otro hilo regrese con éxito de un Join () en ese hilo. • Si una acción A ocurre antes de una acción B, y B ocurre antes de una acción c, entonces A ocurre antes de c.
Traducido como:
• La operación anterior ocurre antes en el mismo hilo. (es decir, en un solo hilo, es legal ejecutar en el orden del código. Sin embargo, el compilador y el procesador pueden reordenar sin afectar los resultados de ejecución en un solo entorno roscado. En otras palabras, esto es que las reglas no pueden garantizar la reordenamiento de la compilación y el reordenamiento de instrucciones).
• Desbloquee la operación en el monitor, antes de su operación de bloqueo posterior. (Reglas sincronizadas)
• Escriba la operación a la variable volátil que ocurre antes de las operaciones de lectura posteriores. (Reglas volátiles)
• El método Start () del hilo ocurre antes de todas las operaciones posteriores del hilo. (Regla de inicio del hilo)
• Todas las operaciones del hilo suceden antes de que otros subprocesos llamen se unan en este hilo y devuelvan la operación exitosa.
• Si se sucede antes de B, B sucede antes de C, entonces una suciedad antes de C (transitiva).
Aquí observamos principalmente la tercera regla: las reglas para garantizar el orden de las variables volátiles. El artículo "Programación de concurrencia de Java: Teoría del núcleo" mencionó que el reordenamiento se divide en reordenamiento del compilador y reordenamiento del procesador. Para implementar la semántica de memoria volátil, JMM restringe la reordenamiento de estos dos tipos de variables volátiles. La siguiente es la tabla de reglas de reordenamiento especificadas por JMM para variables volátiles:
| Puede reordenar | Segunda operación | |||
| Primera operación | Carga normal Tienda normal | Carga volátil | Tienda volátil | |
| Carga normal Tienda normal | No | |||
| Carga volátil | No | No | No | |
| Tienda volátil | No | No | ||
3. Barrera de memoria
Para implementar la visibilidad volátil y la semántica de sucesión. El JVM subyacente se realiza a través de algo llamado "barrera de memoria". La barrera de memoria, también conocida como valla de memoria, es un conjunto de instrucciones de procesador utilizadas para implementar restricciones secuenciales en las operaciones de memoria. Aquí está la barrera de memoria requerida para completar las reglas anteriores:
| Barreras requeridas | Segunda operación | |||
| Primera operación | Carga normal | Tienda normal | Carga volátil | Tienda volátil |
| Carga normal | Tienda de carga | |||
| Tienda normal | Almacén | |||
| Carga volátil | Carga | Tienda de carga | Carga | Tienda de carga |
| Tienda volátil | Seto | Almacén | ||
(1) Barrera de carga de carga
Orden de ejecución: Load1–> Carga de carga -> Load2
Asegúrese de que Load2 y las instrucciones de carga posteriores puedan acceder a los datos cargados por Load1 antes de cargar datos.
(2) barrera de Storestore
Orden de ejecución: Store1‐> Storestore‐> Store2
Asegúrese de que los datos de la operación de Store1 sean visibles para otros procesadores antes de que se ejecuten Store2 y que se ejecuten las instrucciones posteriores de la tienda.
(3) Barrera de carga de carga
Orden de ejecución: Load1–> LoadStore‐> Store2
Asegúrese de que antes de la tienda2 y las instrucciones posteriores de la tienda se ejecuten, se pueden acceder a los datos cargados por Load1.
(4) Barrera de Storeload
Orden de ejecución: Store1‐> storeload‐> Load2
Asegúrese de que antes de la carga2 y las instrucciones de carga posteriores se lean, los datos de Store1 son visibles para otros procesadores.
Finalmente, puedo usar un ejemplo para ilustrar cómo se inserta la barrera de memoria en el JVM:
paquete com.paddx.test.concurrent; public class MemoryBarrier {int a, b; Volátil int v, u; void f () {int i, j; i = a; j = b; i = V; // carga de carga j = u; // LoadStore a = i; b = j; // storestore v = i; // storestore u = j; // storeload i = u; // Loadload // LoadStore J = B; a = i; }}4. Resumen
En general, la comprensión volátil sigue siendo relativamente difícil. Si no lo entiende particularmente bien, no tiene que darse prisa. Se necesita un proceso para comprenderlo completamente. También verá los escenarios de uso de volátiles muchas veces en artículos posteriores. Aquí tengo una comprensión básica del conocimiento básico de volátil y el original. En términos generales, Volatile es una optimización en la programación concurrente, que puede reemplazar sincronizado en algunos escenarios. Sin embargo, Volátil no puede reemplazar completamente la posición de sincronizado. Solo en algunos escenarios especiales se pueden aplicar volátiles. En general, las siguientes dos condiciones deben cumplirse al mismo tiempo para garantizar la seguridad del hilo en un entorno concurrente:
(1) La operación de escritura a las variables no depende del valor actual.
(2) Esta variable no está incluida en el invariante con otras variables.
El artículo anterior sobre programación concurrente de Java: el uso de volátil y su análisis principal es todo el contenido que comparto con usted. Espero que pueda darle una referencia y espero que pueda apoyar más a Wulin.com.