Prefacio
El artículo anterior habló sobre el principio CAS, que mencionó la clase Atomic*. El mecanismo para implementar operaciones atómicas se basa en las características de visibilidad de la memoria de volátiles. Si aún no conoce CAS y Atomic*, se recomienda echar un vistazo a el bloqueo de CAS del que estamos hablando.
Tres características de concurrencia
En primer lugar, si queremos usar volátiles, debe estar en un entorno de concurrencia de múltiples subprocesos. Hay tres características importantes en el escenario concurrente del que a menudo hablamos: atomicidad, visibilidad y orden. Solo cuando se cumplan estas tres características se puede ejecutar el programa concurrente correctamente, de lo contrario surgirán varios problemas.
Atomicidad, las clases CAS y Atomic* mencionadas en el artículo anterior pueden garantizar la atomicidad de las operaciones simples. Para algunas operaciones responsables, se puede implementar utilizando sincronizados o varias cerraduras.
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.
Pedido, el orden de ejecución del programa se ejecuta en el orden del código, y se prohíbe que las instrucciones se reordenen. Parece natural que este no sea el caso. El reordenamiento de instrucciones es el JVM para optimizar las instrucciones y mejorar la eficiencia de la operación del programa, y para mejorar el paralelismo tanto como sea posible sin afectar los resultados de ejecución de un programa único. Sin embargo, en un entorno múltiple, el orden de algunos códigos puede causar incorrección lógica.
Volátil implementa dos características, visibilidad y orden. Por lo tanto, en un entorno múltiple, es necesario garantizar la función de estas dos características, y se puede usar la palabra clave volátil.
Cómo el volátil garantiza la visibilidad
Cuando se trata de visibilidad, debe comprender el procesador de la computadora y la memoria principal. Debido a la multicina, no importa cuántos hilos haya, eventualmente se llevará a cabo en un procesador de computadora. Las computadoras de hoy son básicamente múltiples núcleos, y algunas máquinas incluso tienen múltiples procesadores. Echemos un vistazo al diagrama de estructura de un multiprocesador:
Esta es una CPU con dos procesadores, un cuatro núcleos. Un procesador corresponde a una ranura física, y múltiples procesadores están conectados a través de un bus QPI. Un procesador consta de múltiples núcleos y un caché L3 compartido de múltiples núcleos entre procesadores. Un núcleo contiene registros, caché L1, caché L2.
Durante la ejecución del programa, la lectura y la redacción de datos deben estar involucradas. Todos sabemos que aunque la velocidad de acceso a la memoria ya es muy rápida, todavía es muy inferior a la velocidad de las instrucciones de ejecución de CPU. Por lo tanto, en el núcleo, L1, L2 y L3 se agregan cachés de nivel tres. De esta manera, cuando el programa se está ejecutando, los datos requeridos se copian primero de la memoria principal al caché del núcleo, y después de completar la operación, se escribe en la memoria principal. La siguiente figura es un diagrama esquemático de los datos de acceso a la CPU, desde registros hasta caché y memoria principal e incluso discos duros, la velocidad se está volviendo cada vez más lenta.
Después de comprender la estructura de la CPU, echemos un vistazo al proceso específico de ejecución del programa y tomemos una operación simple de autoincremento como ejemplo.
i = i+1;
Al ejecutar esta declaración, un hilo que se ejecuta en un central copia el valor de I al caché donde se encuentra el núcleo. Después de completar la operación, se volverá a escribir en la memoria principal. En un entorno múltiple, cada subproceso tendrá una memoria de trabajo correspondiente en el área de caché en el núcleo en ejecución, es decir, cada hilo tiene su propio área de caché de trabajo privado para almacenar los datos de réplica requeridos para la operación. Entonces, veamos el problema de i+1. Suponiendo que el valor inicial de I es 0, hay dos hilos que ejecutan esta declaración al mismo tiempo, y cada hilo necesita tres pasos para ejecutar:
1. Lea el valor I de la memoria principal a la memoria de trabajo de subprocesos, es decir, el área de caché del núcleo correspondiente;
2. Calcule el valor de i+1;
3. Escriba el valor del resultado a la memoria principal;
Después de que los dos hilos se ejecutan 10,000 veces cada uno, el valor esperado debe ser de 20,000. Desafortunadamente, el valor de I siempre es inferior a 20,000. Una de las razones de este problema es el problema de consistencia de caché. Para este ejemplo, una vez que se modifica una copia de caché de un subproceso, la copia de caché de otros subprocesos debe invalidarse de inmediato.
Después de usar la palabra clave volátil, los siguientes efectos serán:
1. Cada vez que se modifique la variable, el caché del procesador (memoria de trabajo) se volverá a escribir a la memoria principal;
2. Escribir de nuevo a la memoria principal de una memoria de trabajo hará que el caché del procesador (memoria de trabajo) de otros subprocesos no sea válido.
Debido a que volátil garantiza la visibilidad de la memoria, en realidad utiliza el protocolo MESI que garantiza la consistencia del caché por CPU. Hay muchos contenidos del protocolo MESI, por lo que no lo explicaré aquí. Por favor, échale un vistazo tú mismo. En resumen, se usa la palabra clave volátil. Cuando la modificación de un hilo a la variable volátil se volverá a escribir en la memoria principal de inmediato, lo que hace que la línea de caché de otros subprocesos se invalida, y otros hilos se ven obligados a usar la variable nuevamente, debe leerse desde la memoria principal.
Luego modificamos la variable I anterior con volátil y la ejecutamos nuevamente, cada hilo se ejecutará 10,000 veces. Desafortunadamente, todavía es menos de 20,000. ¿Por qué es esto?
Volátil utiliza el protocolo MESI de la CPU para garantizar la visibilidad. Sin embargo, tenga en cuenta que Volatile no garantiza la atomicidad de la operación, porque esta operación de auto-incremento se divide en tres pasos. Supongamos que el hilo 1 lee el valor I de la memoria principal, suponiendo que sea 10, y un bloqueo ocurre en este momento, pero aún no me he modificado. En este momento, Thread 2 también lee el valor I de la memoria principal. En este momento, el valor i leído por estos dos hilos es el mismo, ambos 10, y luego el hilo 2 agrega 1 a I y lo escribe de nuevo a la memoria principal de inmediato. En este momento, según el protocolo MESI, la línea de caché correspondiente a la memoria de trabajo del subproceso 1 se establecerá en un estado inválido, sí. Sin embargo, tenga en cuenta que Thread 1 ya ha copiado el valor I de la memoria principal, y ahora solo toma la operación de agregar 1 y volver a escribir a la memoria principal. Ambos hilos agregan 1 sobre la base de 10 y luego vuelven a escribir a la memoria principal, por lo que el valor final de la memoria principal es solo 11, no los 12 esperados.
Por lo tanto, el uso de volátiles puede garantizar la visibilidad de la memoria, pero no puede garantizar la atomicidad. Si aún se necesita atomicidad, puede consultar este artículo anterior.
Cómo volátil asegura el orden
El modelo de memoria Java tiene una "línea de orden" innata, es decir, se puede garantizar sin ningún medio. Esto generalmente se llama Principio de Happen-Before. 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.
Los siguientes son 8 principios de HABIERS-BORENES, EXISTRADOS DE "ENTENDIDA DE LA COMENTACIÓN DE MAQUINAS VIRTUALES JAVA".
Aquí hablaremos principalmente sobre las reglas de la palabra clave volátil y daremos un ejemplo de doble verificación en el famoso patrón de singleton:
clase Singleton {Instancia singleton static static volátil privada = nulo; private singleton () {} public static singleton getInStance () {if (instance == null) {// paso 1 sincronizado (singleton.class) {if (instance == null) // paso 2 instancia = nueva singleton (); // Paso 3}} Instancia de retorno; }}Si la instancia no se modifica con volátil, ¿qué resultados se pueden producir? Supongamos que hay dos hilos llamando al método getInstance (). Thread 1 ejecuta el paso 1 y encuentra que la instancia es nula, y luego bloquea la clase Singleton sincrónicamente. Luego determina si la instancia es nula nuevamente, y encuentra que todavía es nula, y luego ejecuta el paso 3 y comienza a instanciar a Singleton. Durante el proceso de instanciación, el hilo 2 va al paso 1 y puede encontrar que la instancia no está vacía, pero en este momento, la instancia no se inicializa por completo.
¿Qué significa? El objeto se inicializa en tres pasos y está representado por el siguiente código de pseudo:
memoria = asignación (); // 1. Asignar el espacio de memoria del objeto ctorInstance (memoria); // 2. Inicializar la instancia del objeto = memoria; // 3. Establezca el espacio de memoria del objeto que apunta al objeto
Debido a que el Paso 2 y el Paso 3 deben depender del Paso 1, y el Paso 2 y el Paso 3 no tienen una dependencia, es posible que estas dos declaraciones se sometan a una reorganización de la instrucción, es decir, o es posible que el Paso 3 se ejecute antes del Paso 2. En este caso, el Paso 3 se ejecuta, pero el Paso 2 aún no se ha ejecutado, es decir, la instancia de la instancia aún no se ha inicializado. Justo ahora, Thread 2 jueces de que la instancia no es nula, por lo que devuelve directamente la instancia de la instancia. Sin embargo, en este momento, la instancia es en realidad un objeto incompleto, por lo que habrá problemas al usarlo.
El uso de la palabra clave volátil significa que usar el principio de "escribir una variable modificada por volátil, antes de leer la variable en cualquier momento posterior" corresponde al proceso de inicialización anterior. Los pasos 2 y 3 son instancias de escritura, por lo que deben ocurrir más tarde cuando lean instancias, es decir, no habrá posibilidad de devolver una instancia que no se inicialice por completo.
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.
por fin
A través de la palabra clave volátil, hemos aprendido sobre la visibilidad y el orden en la programación concurrente, que por supuesto es solo una comprensión simple. Para una comprensión más profunda, debe confiar en sus compañeros de clase para estudiarlo usted mismo.
Artículos relacionados
¿De qué son las cerraduras de spin de CAS de los que estamos hablando?
Resumir
Lo anterior es todo el contenido de este artículo. Espero que el contenido de este artículo tenga cierto valor de referencia para el estudio o el trabajo de todos. Si tiene alguna pregunta, puede dejar un mensaje para comunicarse. Gracias por su apoyo a Wulin.com.