teoria - partes de un hilo en java
Visibilidad de estado de subprocesos en Java: ¿hay alguna manera de convertir la JVM en el peor de los casos? (7)
Anteriormente, sugerí una JVM con el peor caso para una prueba en la lista de modelos de memoria, pero la idea no parecía popular.
Entonces, ¿cómo obtener el "peor caso de comportamiento JVM", con la tecnología existente, es decir, cómo puedo probar el escenario en la pregunta y hacer que falle CADA vez. Podría tratar de encontrar la configuración con el modelo de memoria más débil posible, pero es poco probable que sea perfecto.
Lo que a menudo he considerado es usar una JVM distribuida de forma similar a como creo que funciona Terracotta bajo la cubierta, por lo que su aplicación ahora se ejecuta en varias JVM (remotas o locales) (los subprocesos de la misma aplicación se ejecutan en instancias diferentes). En esta configuración, la comunicación entre hilos JVM se lleva a cabo en barreras de memoria, por ejemplo, las palabras clave sincronizadas que faltan en código con errores (por ejemplo, se ajusta al Modelo de memoria Java) y la aplicación está configurada, es decir, este hilo de clase se ejecuta aquí. No se requiere ningún cambio de código para la configuración de prueba, cualquier aplicación Java bien ordenada debería ejecutarse de forma predeterminada, sin embargo, esta configuración sería muy intolerante con una aplicación mal ordenada (normalmente un problema ... ahora un activo, es decir, el modelo de memoria exhibe comportamiento débil pero legal). En el ejemplo anterior cargando el código en un clúster, si dos subprocesos se ejecutan en nodos diferentes setValue no tiene efecto visible para el otro subproceso a menos que el código se haya cambiado y sincronizado, se hayan utilizado volátiles, etc., entonces el código funciona según lo previsto.
Ahora su prueba para el ejemplo anterior (configurado correctamente) fallaría cada vez sin "pasar antes de ordenar" correcto, que es potencialmente muy útil para las pruebas. El error en el plan de cobertura completa requeriría un nodo potencialmente por cada subproceso de aplicación (puede ser la misma máquina o múltiple en un clúster) o múltiples ejecuciones de prueba. Si tiene miles de subprocesos, eso podría ser prohibitivo, aunque es de esperar que se agrupen y reduzcan para escenarios de prueba E2E o que se ejecuten en una nube. Si nada más, este tipo de configuración podría ser útil para demostrar el problema.
Supongamos que nuestro código tiene 2 hilos (A y B) tienen una referencia a la misma instancia de esta clase en alguna parte:
public class MyValueHolder {
private int value = 1;
// ... getter and setter
}
Cuando el hilo A hace myValueHolder.setValue(7)
, no hay garantía de que el hilo B leerá ese valor alguna vez: myValueHolder.getValue()
podría - en teoría - seguir devolviendo 1
para siempre.
En la práctica, sin embargo, el hardware borrará el caché de segundo nivel tarde o temprano, por lo que el hilo B leerá 7
tarde o temprano (generalmente antes).
¿Hay alguna manera de hacer que la JVM emule el peor de los casos para los que siempre devuelve 1
para el subproceso B? Eso sería muy útil para probar nuestro código de subprocesos múltiples con nuestras pruebas existentes en esas circunstancias.
No en una máquina real, lamentablemente probar el código de subprocesos múltiples seguirá siendo difícil.
Como dices, el hardware borrará la memoria caché de segundo nivel y la JVM no tiene control sobre eso. El JSL solo especifica lo que debe suceder y este es un caso donde B
podría no ver el valor actualizado del value
.
La única forma de obligar a que esto suceda en una máquina real es modificar el código de tal forma que anule su estrategia de prueba, es decir, termine probando un código diferente.
Sin embargo, es posible que pueda ejecutar esto en un simulador que simula hardware que no borra la memoria caché de segundo nivel. Aunque suena como un gran esfuerzo!
Aquí hay una manera simple: simplemente comente el código para setValue
. Puedes descomentarlo después de la prueba. Como en muchos casos como este, se necesita un mecanismo para falsificar fallas, sería una buena idea construir un mecanismo general para todos esos casos.
Creo que se está refiriendo al principio denominado "uso compartido falso" en el que diferentes CPU deben sincronizar sus memorias caché o enfrentar la posibilidad de que los datos como usted describan podrían no coincidir. Hay un muy buen artículo sobre el intercambio falso en el sitio web de Intel. Intel describe algunas herramientas útiles en su artículo para diagnosticar este problema. Esta es una cita relevante:
El principal medio para evitar el intercambio falso es a través de la inspección del código. Las instancias donde los subprocesos acceden a estructuras de datos compartidas globales o dinámicamente asignadas son fuentes potenciales de uso compartido falso. Tenga en cuenta que la compartición falsa puede oscurecerse por el hecho de que los subprocesos pueden acceder a variables globales completamente diferentes que se encuentran relativamente juntas en la memoria. El almacenamiento local de subprocesos o las variables locales se pueden descartar como fuentes de uso compartido falso.
Aunque los métodos descritos en el artículo no son lo que usted solicitó (forzando el peor caso de comportamiento de la JVM), como ya se dijo, esto no es realmente posible. Los métodos que se describen en este artículo son la mejor forma que conozco para intentar diagnosticar y evitar el uso compartido falso.
Hay otros recursos que abordan este problema en la web. Por ejemplo, este artículo tiene una sugerencia para una forma de evitar compartir falsamente en Java. No he probado este método, por lo que no puedo responderlo, pero creo que la idea del autor es sólida. Puede considerar probar su sugerencia.
Sería grandioso tener un compilador Java que intencionalmente realizara tantas transfiguraciones raras (pero permitidas) como sea posible para poder romper el código inseguro del hilo más fácilmente, como Csmith para C. Desafortunadamente, tal compilador no existe (en la medida de lo posible) que yo sé).
Mientras tanto, puedes probar la biblioteca jcstress * y ejercitar tu código en varias arquitecturas, si es posible con modelos de memoria más débiles (es decir, no x86) para intentar descifrar tu código:
Las pruebas de Java Concurrency Stress (jcstress) son un arnés experimental y un conjunto de pruebas que ayudan a la investigación en la corrección del soporte de simultaneidad en JVM, bibliotecas de clases y hardware.
Pero, al final, desafortunadamente, la única forma de probar que un fragmento de código es 100% correcto es la inspección del código (y no conozco ninguna herramienta de análisis de código estático capaz de detectar todas las condiciones de carrera).
* No lo he usado y no estoy seguro de cuál de jcstress y la biblioteca de java-concurrency-torture está más actualizada (sospecho que jcstress).
jcstress mantenedor aquí. Hay varias formas de responder esa pregunta.
- La solución más fácil sería envolver el getter en el bucle, y dejar que JIT lo alce. Esto está permitido para lecturas de campos no volátiles, y simula el error de visibilidad con la optimización del compilador.
- El truco más sofisticado consiste en obtener la compilación de depuración de OpenJDK, y usar
-XX:+StressLCM -XX:+StressGCM
, haciendo de manera efectiva el fuzzing de programación de instrucciones. Lo más probable es que la carga en cuestión flote en algún lugar que pueda detectar con las pruebas regulares que tiene su producto. - No estoy seguro de si hay hardware práctico que contenga el valor escrito suficientemente opaco para guardar coherencia en la memoria caché, pero es algo fácil construir el caso de prueba con jcstress. Debe tener en cuenta que la optimización en (1) también puede ocurrir, por lo que debemos emplear un truco para evitar eso. Creo que algo así debería funcionar.
El ejemplo que ha proporcionado se describe como Sincronizado incorrectamente en http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.4 . Creo que esto siempre es incorrecto y dará lugar a errores tarde o temprano. La mayoría de las veces después :-).
Para encontrar bloques de código incorrectamente sincronizados, uso el siguiente algoritmo:
Registre los hilos para todas las modificaciones de campo usando instrumentación. Si un campo es modificado por más de un hilo sin sincronización, he encontrado una carrera de datos.
Implementé este algoritmo dentro de http://vmlens.com , que es una herramienta para encontrar razas de datos dentro de los programas de Java.