Muchos materiales en línea describen el modelo de memoria Java, que introducirá que hay una memoria principal, y cada hilo de trabajadores tiene su propia memoria de trabajo. Habrá una pieza de datos en la memoria principal y una pieza en la memoria de trabajo. Habrá varias operaciones atómicas entre la memoria de trabajo y la memoria principal para sincronizar.
La siguiente imagen es de este blog
Sin embargo, debido a la evolución continua de la versión Java, el modelo de memoria también ha cambiado. Este artículo solo habla sobre algunas características del modelo de memoria Java. Ya sea que se trate de un nuevo modelo de memoria o un modelo de memoria antiguo, se verá más claro después de comprender estas características.
1. Atomicidad
La atomicidad significa que una operación es ininterrumpida. Incluso cuando se ejecutan múltiples hilos juntos, una vez que se inicia una operación, no se verá perturbado por otros hilos.
En general, se cree que las instrucciones de la CPU son operaciones atómicas, pero el código que escribimos no es necesariamente operaciones atómicas.
Por ejemplo, i ++. Esta operación no es una operación atómica, se divide básicamente en 3 operaciones, lee I, realiza +1 y asigna valor a i.
Supongamos que hay dos hilos. Cuando el primer hilo se lee i = 1, la operación +1 aún no se ha realizado y cambia al segundo hilo. En este momento, el segundo hilo también se lee i = 1. Luego, los dos subprocesos realizan operaciones +1 posteriores y luego asignan valores hacia atrás, I no es 3, sino 2. Obviamente hay inconsistencia en los datos.
Por ejemplo, leer un valor largo de 64 bits en un JVM de 32 bits no es una operación atómica. Por supuesto, JVM de 32 bits lee enteros de 32 bits como una operación atómica.
2. Orden
Durante la concurrencia, la ejecución del programa puede estar fuera de servicio.
Cuando una computadora ejecuta código, no necesariamente se ejecuta en el orden del programa.
clase OrderSexample {int a = 0; bandera booleana = falso; Public void Writer () {a = 1; bandera = verdadero; } public void Reader () {if (flag) {int i = a +1; }}} Por ejemplo, en el código anterior, dos métodos son llamados por dos hilos respectivamente. Según el sentido común, el hilo de escritura primero debe ejecutar A = 1, y luego ejecutar flag = true. Cuando el hilo de lectura está leyendo, i = 2;
Pero debido a que a = 1 y flag = verdadero, no hay correlación lógica. Por lo tanto, es posible revertir el orden de ejecución, y es posible ejecutar flag = true primero, y luego a = 1. En este momento, cuando Flag = verdadero, cambie al hilo de lectura. En este momento, A = 1 aún no se ha ejecutado, entonces el hilo de lectura será i = 1.
Por supuesto que esto no es absoluto. Es posible que haya fuera de servicio y no suceda.
Entonces, ¿por qué hay fuera de servicio? Esto comienza con la instrucción de la CPU. Después de compilar el código en Java, finalmente se convierte en el código de ensamblaje.
La ejecución de una instrucción se puede dividir en muchos pasos. Suponiendo que la instrucción de la CPU se divide en los siguientes pasos
Supongamos que hay dos instrucciones aquí
En términos generales, pensaremos que las instrucciones se ejecutan en serie, primero ejecutan la Instrucción 1 y luego ejecute la Instrucción 2. Suponiendo que cada paso requiere 1 período de tiempo de CPU, luego ejecutar estas dos instrucciones requiere 10 períodos de CPU, lo que es demasiado ineficiente para hacerlo. De hecho, las instrucciones se ejecutan en paralelo. Por supuesto, cuando la primera instrucción se ejecuta si, la segunda instrucción no puede realizar si porque la instrucción se registra y similares no se pueden ocupar al mismo tiempo. Entonces, como se muestra en la figura anterior, las dos instrucciones se ejecutan en paralelo de manera relativamente escalonada. Cuando la instrucción 1 ejecuta la ID, la instrucción 2 ejecuta el IF. De esta manera, se ejecutaron dos instrucciones en solo 6 períodos de tiempo de CPU, lo cual fue relativamente eficiente.
Según esta idea, echemos un vistazo a cómo se ejecuta la instrucción A = B+C.
Como se muestra en la figura, hay una operación inactiva (x) durante la operación ADD, porque cuando desea agregar B y C, cuando la operación X de ADD en la figura, C no ha leído desde la memoria (c solo se lee desde la memoria cuando la operación de MEM se completa. Hay una pregunta aquí. En este momento, no hay redes de redacción (WB) a R2, ¿cómo se puede agregar R1 y R1? No hay necesidad de esperar a que el WB ejecute antes de que se realice ADD). Por lo tanto, habrá un tiempo inactivo (x) en la operación de agregar. En la operación SW, dado que la instrucción EX no se puede realizar simultáneamente con la instrucción ADD EX, habrá un tiempo inactivo (x).
A continuación, damos un ejemplo un poco más complicado
a = b+c
d = EF
Las instrucciones correspondientes son las siguientes
La razón es similar a la anterior, por lo que no lo analizaré aquí. Descubrimos que hay mucha X aquí, y hay muchos ciclos de tiempo desperdiciados, y el rendimiento también se ve afectado. ¿Hay alguna forma de reducir la cantidad de XS?
Esperamos usar algunas operaciones para llenar el tiempo libre de X, porque ADD tiene dependencia de datos con las instrucciones anteriores, y esperamos usar algunas instrucciones sin dependencia de datos para llenar el tiempo libre generado por la dependencia de los datos.
Cambiamos el orden de las instrucciones
Después de cambiar el orden de las instrucciones, se elimina X. El período de tiempo de ejecución general también ha disminuido.
El reordenamiento de instrucciones puede hacer que la tubería sea más suave
Por supuesto, el principio de reorganización de instrucciones es que no puede destruir la semántica del programa en serie. Por ejemplo, a = 1, b = a+1, tales instrucciones no se reorganizarán porque el resultado en serie del reordenamiento es diferente del original.
El reordenamiento de instrucciones es solo una forma de optimizar el compilador o la CPU, y esta optimización ha causado problemas con el programa al comienzo de este capítulo.
¿Cómo resolverlo? Use la palabra clave volátil, se introducirá esta serie posterior.
3. Visibilidad
La visibilidad se refiere a si otros subprocesos pueden conocer inmediatamente la modificación cuando un hilo modifica el valor de una variable compartida.
Pueden surgir problemas de visibilidad en varios enlaces. Por ejemplo, el reordenamiento de instrucciones recién mencionado también causará problemas de visibilidad y, además, la optimización del compilador u optimización de cierto hardware también causará problemas de visibilidad.
Por ejemplo, un hilo optimiza un valor compartido en la memoria, mientras que otro hilo optimiza el valor compartido en el caché. Al modificar el valor en la memoria, el caché no conoce la modificación.
Por ejemplo, algunas optimizaciones de hardware, cuando un programa escribe varias veces en la misma dirección, pensará que no es necesario y solo mantiene la última escritura, por lo que los datos escritos antes serán invisibles en otros hilos.
En resumen, la mayoría de los problemas con visibilidad provienen de la optimización.
A continuación, veamos un problema de visibilidad que surge del nivel de máquina virtual de Java
El problema proviene de un blog
paquete edu.hushi.jvm; /** * * * @author -10 * */public class VisibilityTest extiende hilo {parada booleana privada; public void run () {int i = 0; while (! stop) {i ++; } System.out.println ("Final Loop, i =" + i); } public void stopit () {stop = true; } public boolean getstop () {return stop; } public static void main (string [] args) lanza la excepción {VisibilityTest v = new VisibilityTest (); V.Start (); Thread.sleep (1000); v.stopit (); Thread.sleep (2000); System.out.println ("Finalizar principal"); System.out.println (V.getStop ()); }} El código es muy simple. El hilo V mantiene I ++ en el bucle While hasta que el hilo principal llama al método de parada, cambiando el valor de la variable de parada en el hilo V para detener el bucle.
Los problemas ocurren cuando se ejecuta el código aparentemente simple. Este programa puede evitar que los subprocesos realicen operaciones de autoincremento en modo cliente, pero en modo de servidor, primero será un bucle infinito. (Más optimización de JVM en modo servidor)
La mayoría de los sistemas de 64 bits son modo de servidor y se ejecutan en modo servidor:
terminar principal
verdadero
Solo se imprimirán estas dos oraciones, pero el bucle de finalización no se imprimirá. Pero puede encontrar que el valor de parada ya es cierto.
El autor de este blog utiliza herramientas para restaurar el programa al código de ensamblaje.
Solo una parte del código de ensamblaje se intercepta aquí, y la parte roja es la parte del bucle. Se puede ver claramente que solo 0x0193bf9d es la verificación de parada, mientras que la parte roja no toma el valor de parada, por lo que se realiza un bucle infinito.
Este es el resultado de la optimización JVM. ¿Cómo evitarlo? Al igual que el reordenamiento de la directiva, use la palabra clave volátil.
Si se agrega volátil, restaurarlo al código de ensamblaje y encontrará que cada bucle obtendrá el valor de parada.
A continuación, echemos un vistazo a algunos ejemplos en la "Especificación del idioma Java"
La figura anterior muestra que el reordenamiento de instrucciones conducirá a diferentes resultados.
La razón por la cual R5 = R2 se hace en la figura anterior es que R2 = R1.x, R5 = R1.x, y está directamente optimizado a R5 = R2 en el tiempo de compilación. Al final, los resultados son diferentes.
4. HACER ANTES
5. El concepto de seguridad de hilos
Se refiere al hecho de que cuando se llama a una determinada biblioteca de funciones o funciones en un entorno de subproceso múltiple, puede procesar correctamente las variables locales de cada subproceso y permitir que las funciones del programa se completen correctamente.
Por ejemplo, el ejemplo I ++ mencionado al principio
Esto conducirá a la insegura de hilos.
Para obtener detalles sobre la seguridad del hilo, consulte este blog que escribí antes o siga la serie posterior, y también hablará sobre contenido relacionado.