Profundice en la capa inferior de Java: explicación detallada de las barreras de memoria y la concurrencia de JVM
La barrera de la memoria, también conocida como barrera de la memoria, es un conjunto de instrucciones del procesador que se utilizan para implementar restricciones secuenciales en las operaciones de la memoria. Este artículo supone que el lector domina completamente los conceptos relevantes y el modelo de memoria de Java. no analiza el mecanismo paralelo concurrente, mutuamente excluyente y las barreras de memoria sexual se utilizan para lograr un papel igualmente importante en la programación concurrente llamada visibilidad
¿Por qué son importantes las barreras de memoria?
Un acceso a la memoria principal normalmente requiere cientos de ciclos de reloj de hardware. Los procesadores pueden reducir el costo de la latencia de la memoria en órdenes de magnitud mediante el almacenamiento en caché. Es decir, reorganizar el orden de las operaciones de memoria pendientes. es decir, las operaciones de lectura y escritura del programa no necesariamente se ejecutarán en el orden que requiere el procesador. Cuando los datos son inmutables y/o los datos están limitados al alcance del hilo, estas optimizaciones son inofensivas.
Si estas optimizaciones se combinan con procesamiento múltiple simétrico y estado mutable compartido, se convierte en una pesadilla cuando las operaciones de memoria basadas en el estado mutable compartido se reordenan y el programa puede comportarse de manera errática en un subproceso. Los datos escritos pueden ser visibles para otros subprocesos porque. el orden de escritura de datos es inconsistente. Las barreras de memoria colocadas correctamente evitan este problema al obligar al procesador a realizar operaciones de memoria pendientes de forma secuencial.
Función de coordinación de las barreras de memoria
p>
La JVM no expone directamente las barreras de memoria, sino que las inserta en la secuencia de instrucciones para mantener la semántica de las primitivas de concurrencia a nivel de lenguaje. Estudiamos el código fuente y las instrucciones de ensamblaje de varios programas Java simples. Primero, un vistazo rápido a las barreras de memoria en el algoritmo de Dekker. El algoritmo utiliza variables volátiles para coordinar el acceso a recursos compartidos entre dos subprocesos.
Preste atención a los excelentes detalles del algoritmo. Cada subproceso intenta ingresar al área crítica en la primera línea de código mediante señalización. Si el subproceso detecta un conflicto en la tercera línea (ambos subprocesos tienen que acceder a él), se resuelve mediante la operación de la variable de giro. Solo un subproceso puede hacerlo. acceder al área crítica en cualquier momento
// código ejecutado por el primer hilo // código ejecutado por el segundo hilo
intentFirst = true;? ;
While (intentSecond)? while (intentFirst)? // lectura volátil
if (turn != ) {? {? // lectura volátil
p>
? intenciónPrimera = falsa; intenciónSegunda = falsa
? }
? intenciónPrimera = verdadera; intenciónSegunda = verdadera;
} }
sección crítica();
turn = ; turn = ; // escritura volátil
intentFirst = false;? intentSecond = false; > Las optimizaciones de hardware pueden alterar la ausencia de barreras de memoria. Este código, incluso si el compilador enumera todas las operaciones de memoria en el orden que desea el programador, considere el volátil dos veces secuencial en la tercera y cuarta líneas.
Cada hilo en la operación de lectura verifica si otros hilos han señalado que quieren ingresar al área crítica y luego verifica a quién le toca operar. Considere dos operaciones de escritura secuenciales en la línea 1. Cada hilo libera el acceso al otro hilo y. luego revoca su intención de acceder al área crítica. Un hilo de lectura nunca debe esperar observar otros hilos escribiendo en la variable de turno después de que el otro hilo haya revocado el acceso. Esto es un desastre.
Pero sucede si estos. ¡Las variables no tienen el modificador volátil! Por ejemplo, sin el modificador volátil, el segundo hilo puede observar la escritura del primer hilo en intentFirst (la penúltima línea) antes de que el primer hilo escriba en turn (la penúltima línea). La palabra clave volatile evita. Esto sucede porque crea una secuencia entre las escrituras. a la variable de turno y la escritura a la variable intentFirst. El compilador no puede reordenar estas escrituras. Utiliza una barrera de memoria para evitar que el procesador reordene si es necesario. Veamos algunos detalles de implementación. La opción HotSpot es un indicador de diagnóstico de la JVM que nos permite obtener las instrucciones de ensamblaje generadas por el compilador JIT. Esto requiere la última versión de OpenJDK o la nueva actualización de HotSpot o superior al requerir un complemento de descompilación. El proyecto Kenai proporciona un complemento. en binarios para Solaris Linux y BSD hsdis es otro complemento que se puede crear desde el código fuente en Windows
Ensamblaje de la primera (tercera línea) de dos operaciones de lectura secuencial. Las instrucciones son las siguientes. se basa en la actualización JDK del hardware de procesamiento múltiple de Itanium. Todos los flujos de instrucciones en este artículo están marcados con números de línea en el lado izquierdo. Se recomienda a los lectores que no se den por vencidos. en cada línea de instrucciones
? x de c:? agrega r = r ;;?
? b a a
? x de a :? nop m x
? x de ac:? p>
? x de b :? nop i x ;
? Enriquecer el primer volátil en la segunda línea del modelo de memoria Java garantiza que la JVM entregará la primera operación de lectura al procesador antes de la segunda operación de lectura, es decir, en el orden del programa, pero esta única línea de instrucciones no es suficiente porque el procesador aún puede ejecutar estas operaciones libremente fuera de orden. Para respaldar la coherencia del modelo de memoria Java. JVM agrega la anotación ld acq en la primera operación de lectura, que es la adquisición de carga. El compilador garantiza la lectura de la segunda línea utilizando ld acq. El problema se resuelve si la operación se completa antes de la siguiente operación de lectura.
Tenga en cuenta que esto afecta la operación de lectura y no la operación de escritura. La barrera de memoria impone el límite de orden de operación de lectura o escritura y no fuerza el límite de orden de operación de lectura o escritura en una dirección. similar a una valla abierta de dos vías. El uso de ld acq es un ejemplo de una barrera de memoria unidireccional.
La coherencia tiene dos lados si un hilo de lectura inserta una barrera de memoria entre dos operaciones de lectura y otra. ¿Qué es bueno para un hilo sin barrera de memoria entre escrituras? Los subprocesos deben cumplir simultáneamente este acuerdo para poder coordinarse, al igual que los nodos de una red o los miembros de un equipo. Si un subproceso rompe este acuerdo, los esfuerzos de todos los demás subprocesos son en vano. Las dos últimas líneas de instrucciones de ensamblaje de Dekker. El algoritmo debe insertar una barrera de memoria entre dos escrituras volátiles
$ java XX:+UnlockDiagnostic.
VMOptions XX:PrintAssemblyOptions=hsdis imprimir bytes
XX:CompileCommand=imprimir WriterReader escribir WriterReader
? x de c :? p> ? x de c :? st rel [r ]=r ;
? ;
? x de dc:? nop i x ;;?
? :? mov ret b =r x de e
? pfs=r ; aa
? Puede ver que la segunda operación de escritura en la cuarta línea está anotada con un explícito. Al usar st rel (versión de tienda), el compilador garantiza que la primera operación de escritura se complete antes de la segunda operación de escritura. Esto completa el acuerdo en ambas partes porque la primera. La operación de escritura ocurre antes de la segunda operación de escritura. Ocurre
La barrera st rel es unidireccional como ld acq, pero en la quinta línea el compilador establece una instrucción mf de barrera de memoria bidireccional, o barrera de memoria. , que es una valla completa en el conjunto de instrucciones de Itanium. Creo que es redundante.
Las barreras de memoria son específicas del hardware
Este artículo no pretende proporcionar una descripción general de toda la memoria. barreras. Sería una hazaña monumental, pero es importante darse cuenta de que estas instrucciones tienen diferentes funciones. La arquitectura del hardware es muy diferente. Las siguientes instrucciones son el resultado de operaciones de escritura secuencial compiladas en hardware Intel Xeon multiprocesamiento. las instrucciones del siguiente artículo son de Intel Xeon a menos que se indique lo contrario
? x f c: push? %ebp ? >
? x f : mov $ x c %edi ; bf c
? $ x x a f (%edi)?
? x f : mov $ x %ebp ; bd
? mov $ x d %edx ; ba d
? %ebx?; fbe af
? x f: prueba? %ebx % ebx ; movl? $ x x a f (% ebp)? ; c d a af
? >
? x f b: agregar $ x %esp ; c
? x f e: pop %ebp ;
Xeon realiza dos escrituras volátiles en la línea 1. La segunda escritura va seguida de una operación mfence y las siguientes escrituras consecutivas se basan en SPARC
xfb ecc: ldub? [ %l + x ] %l ; e c
xfb ecc : cmp? %l ; a e
xfb ecc c: bne pn? ;
xfb ecc : st? %l [ %l + x ] ; e
xfb ecc : clrb? > xfb ecc c: membar? #StoreLoad ; e
xfb ecca : sethi? %hi( xff fc ) %l ; %g ; c
xfb ecca : ret ; c e
xfb eccac ; en las líneas 56. La segunda operación de escritura va seguida de una instrucción membar. Una barrera de memoria bidireccional explícita. Existe una diferencia importante entre el flujo de instrucciones de x y SPARC y el flujo de instrucciones de Itanium. x y SPARC. Seguimiento de operaciones de escritura consecutivas, pero no se coloca ninguna barrera de memoria entre dos operaciones de escritura.
Por otro lado, el flujo de instrucciones de Itanium tiene una barrera de memoria entre dos operaciones de escritura. ¿Por qué la JVM se comporta en diferentes hardware? ¿Arquitecturas? ¿No es lo mismo? Debido a que la arquitectura del hardware tiene su propio modelo de memoria, cada modelo de memoria tiene un conjunto de garantías de coherencia. Algunos modelos de memoria, como x y SPARC, tienen garantías de coherencia sólidas, mientras que otros modelos de memoria, como Itanium PowerPC y Alpha, tienen una garantía débil. >
Por ejemplo, x y SPARC no reordenarán las escrituras consecutivas, por lo que no hay necesidad de colocar una barrera de memoria. Itanium PowerPC y Alpha reordenarán las escrituras consecutivas, por lo que la JVM debe colocar una barrera de memoria entre las dos. La JVM usa barreras de memoria para reducir Java. memoria La distancia entre el modelo y el modelo de memoria del hardware
Barreras de memoria implícitas
Las instrucciones de barrera explícitas no son la única forma de serializar operaciones de memoria. Echemos otro vistazo al ejemplo de. Clase de contador
clase Contador{
static int contador =
public static void main( Cadena[] _){
for(int i = ; i < ; i++)
inc()
}
static sincronizado void inc(){ contador += }
}
La clase Counter realiza una lectura y modificación típicas. y operación de escritura El campo del contador estático no es volátil porque las tres operaciones deben ser visibles atómicamente, por lo que el método inc está sincronizado. Podemos usar el siguiente comando para compilar la clase Counter y ver las instrucciones de ensamblaje generadas. la salida del área sincronizada y las operaciones de memoria volátil tienen la misma visibilidad, por lo que deberíamos esperar otra barrera de memoria.
$ java XX:+UnlockDiagnosticVMOptions XX:PrintAssemblyOptions=hsdis print bytes
XX: UseBiasedLocking XX:CompileCommand=print Counter inc Counter
? ? %ebp? ;
? x d eda : mov %esp %ebp ; bec
? p> ? x d edb: lea x (%esp) %edi;
%edi
? >
? x d edbe: mov %eax (%edi);
? x d edc: bloquear cmpxchg %edi (%esi)?; edc: je x d edda ; f
? x d edca: sub %esp %eax ; bc
? %edi) ;
? x d edd : jne x d ee ; f
? x d edda: mov $ x ba b %eax ; x d eddf: mov x (%eax) %esi? ; bb
? x (%eax)?; b
? d
? /p>
? x d edf : prueba? %esi %esi ; > ? eax) %edi; b
? x d edfd: bloquear cmpxchg %esi (%edi)? esp?; be
? Se realiza una operación de incremento en la línea 1, pero la JVM no inserta explícitamente la barrera de memoria. En cambio, la JVM mata dos pájaros de un tiro usando el prefijo de bloqueo de cmpxchg. líneas 1 y 2. La semántica de cmpxchg está más allá del alcance de este artículo
lock cmpx
chg no solo realiza escrituras atómicas sino que también vacía las lecturas y escrituras pendientes. Las escrituras ahora se completarán antes de todas las operaciones de memoria posteriores. Se verá el mismo truco si refactorizamos y ejecutamos Counter mediante ncurrent atomic AtomicInteger
import ncurrent atomic. AtomicInteger;
clase Contador{
contador AtomicInteger estático = nuevo AtomicInteger(
<); /p>
public static void main(String[] args){
for(int i = ; i < ; i++)
contador incrementAndGet(); >
}
}
$ java XX:+UnlockDiagnosticVMOptions XX:PrintAssemblyOptions=hsdis imprimir bytes
XX:CompileCommand=print *AtomicInteger incrementAndGet Contador
? x f : push? %ebp ; >
? x fa: sub $ x %esp ; ec
? x fd: jmp x a ; >
? x : prueba? %eax xb e ; e b
? x a: mov x (%ecx) %eax ; ? bf
? x f: inc %esi
? %edi) %edi? ; bbf
? x b: mov %ecx %edi ; bf
? > ? x : bloquear cmpxchg %esi (%edi)? ; f fb
? mov $ x %eax ; p>
? mov $ x %eax ; b
? p>
? mov %esi %eax? ; bc
? p> Una vez más vemos una operación de escritura con el prefijo de bloqueo en la línea 1. Esto garantiza que el nuevo valor de la variable (la operación de escritura) se completará antes que todas las demás operaciones de memoria posteriores
Las barreras de memoria pueden debe evitarse
La JVM es muy buena para eliminar barreras de memoria innecesarias. Por lo general, la JVM tiene suerte porque el hardware.
La garantía de coherencia del modelo de memoria es más fuerte o igual que el modelo de memoria de Java. En este caso, la JVM simplemente inserta una declaración no op en lugar de una barrera de memoria real.
Por ejemplo, la garantía de coherencia de. los modelos de memoria x y SPARC son lo suficientemente fuertes como para eliminar la barrera de memoria requerida al leer variables volátiles. ¿Recuerda la barrera de memoria unidireccional explícita entre lecturas en Itanium? No existe una barrera de memoria entre instrucciones de ensamblaje para operaciones de lectura volátiles consecutivas en el algoritmo de Dekker en x. Operaciones de lectura consecutivas de memoria compartida en p>
? x f : mov $ x d %edx
? x f c: mov *** l x a f (%edx) %ebx ; fbe a daf
? x f : %ebx %ebx ; jne x f ;
? >
? x f : movb ? $ x x a f (%edi)? ?
? x f e: pop %ebp?; d
? x f f: prueba? %eax xb ec; ? x f : nopw? x (%eax %eax )? ; f f
? La disminución tiene poco impacto en la optimización del código. son inherentemente mejores que las barreras bidireccionales. JVM evitará el uso de barreras bidireccionales cuando garantice que las barreras unidireccionales sean suficientes. El primer ejemplo de este artículo demuestra esto. Dos operaciones de lectura consecutivas en la plataforma Itanium *** insertan un uno. Barrera de memoria bidireccional Si la operación de lectura inserta una barrera de memoria bidireccional explícita, el programa seguirá siendo correcto pero el retraso será mayor
Compilación dinámica
Todo lo que sea estático. El compilador decide en la etapa de compilación se puede decidir en tiempo de ejecución con un compilador dinámico y aún más. Más información significa que hay más oportunidades para optimizar. Por ejemplo, veamos cómo se tratan las barreras de memoria en tiempo de ejecución de uniprocesador. de un compilador en tiempo de ejecución que implementa dos escrituras volátiles consecutivas utilizando el algoritmo Dekker. Una imagen de estación de trabajo VMWare que se ejecuta en modo monoprocesador en x hardware
? x b c: %ebp? x b d: sub $ x %esp? ; ec
? x b : movb $ x f ; %ebp ;bd
?edx ;bad
? x b : mov *** l x f (%edx) %ebx ; fbe a d aaf
? x b : %ebx %ebx ; x b ; c
? x b : movl? $ x x f (%ebp)? ; c d aaf
? ? Todos están seguidos por una barrera. JVM ha realizado optimizaciones similares para los condicionales atómicos. El siguiente flujo de instrucciones proviene del resultado de compilación dinámica AtomicInteger de la misma imagen de VMWare
: ¿push? ;
? x f : mov %esp %ebp ; bec
? ; e
p>? x : xchg
? x (%ecx) % eax; b
? x d: mov %eax %esi ; bf
? edi? > ? x d: agregar $ x %edi? ; c
? je x ; f
? x e: mov $ x %eax ; b
? je x ; cc
? x : mov %esi %eax ; bc
? el costo es algo diferente al de la compilación estática
Conclusión lishixinzhi/Article/program/Java/hx/201311/25723