Una operación sensible en mi laboratorio hoy salió completamente mal. Un actuador en un microscopio electrónico superó sus límites, y después de una cadena de eventos perdí $ 12 millones en equipos. He reducido más de 40K líneas en el módulo defectuoso a esto:
import java.util.*;
class A {
static Point currentPos = new Point(1,2);
static class Point {
int x;
int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
}
public static void main(String[] args) {
new Thread() {
void f(Point p) {
synchronized(this) {}
if (p.x+1 != p.y) {
System.out.println(p.x+" "+p.y);
System.exit(1);
}
}
@Override
public void run() {
while (currentPos == null);
while (true)
f(currentPos);
}
}.start();
while (true)
currentPos = new Point(currentPos.x+1, currentPos.y+1);
}
}
Algunas muestras de la salida que estoy obteniendo:
$ java A
145281 145282
$ java A
141373 141374
$ java A
49251 49252
$ java A
47007 47008
$ java A
47427 47428
$ java A
154800 154801
$ java A
34822 34823
$ java A
127271 127272
$ java A
63650 63651
Como no hay ninguna aritmética de coma flotante aquí, y todos sabemos que los enteros con signo se comportan bien en el desbordamiento en Java, creo que no hay nada de malo en este código. Sin embargo, a pesar de que la salida indica que el programa no alcanzó la condición de salida, alcanzó la condición de salida (¿se alcanzó y no se alcanzó?). ¿Por qué?
Me di cuenta de que esto no sucede en algunos entornos. Estoy en OpenJDK 6 en Linux de 64 bits.
final
calificador (que no tiene ningún efecto sobre el código de bytes producido) a los camposx
yy
"resuelve" el error. Aunque no afecta el código de bytes, los campos están marcados con él, lo que me lleva a pensar que esto es un efecto secundario de una optimización JVM.Point
p
construye un A que satisfacep.x+1 == p.y
, luego se pasa una referencia al hilo de votación. Finalmente, el hilo de sondeo decide salir porque cree que la condición no se cumple para uno de losPoint
mensajes que recibe, pero luego la salida de la consola muestra que debería haberse cumplido. La falta devolatile
aquí simplemente significa que el hilo de votación puede atascarse, pero ese claramente no es el problema aquí.synchronized
hace que el error no ocurra. Eso es porque tuve que escribir código al azar hasta que encontré uno que reprodujera este comportamiento de manera determinista.Respuestas:
currentPos = new Point(currentPos.x+1, currentPos.y+1);
hace algunas cosas, incluyendo escribir valores predeterminados enx
yy
(0) y luego escribir sus valores iniciales en el constructor. Como su objeto no se publica de manera segura, el compilador / JVM puede reordenar libremente esas 4 operaciones de escritura.Entonces, desde la perspectiva del hilo de lectura, es una ejecución legal leer
x
con su nuevo valor peroy
con su valor predeterminado de 0, por ejemplo. Cuando llega a laprintln
declaración (que por cierto está sincronizada y, por lo tanto, influye en las operaciones de lectura), las variables tienen sus valores iniciales y el programa imprime los valores esperados.Marcar
currentPos
comovolatile
garantizará una publicación segura ya que su objeto es efectivamente inmutable: si en su caso de uso real el objeto está mutado después de la construcción, lasvolatile
garantías no serán suficientes y podría ver un objeto inconsistente nuevamente.Alternativamente, puede hacer el
Point
inmutable que también garantizará una publicación segura, incluso sin usarvolatile
. Para lograr la inmutabilidad, simplemente necesita marcarx
yy
finalizar.Como nota al margen y como ya se mencionó,
synchronized(this) {}
la JVM puede tratarlo como no operativo (entiendo que lo incluyó para reproducir el comportamiento).fuente
Como
currentPos
se está cambiando fuera del hilo, debe marcarse comovolatile
:Sin volátil, el hilo no está garantizado para leer en las actualizaciones de CurrentPos que se están haciendo en el hilo principal. Por lo tanto, se siguen escribiendo nuevos valores para currentPos, pero el hilo continúa usando las versiones anteriores en caché por razones de rendimiento. Como solo un subproceso modifica currentPos, puede escapar sin bloqueos, lo que mejorará el rendimiento.
Los resultados se ven muy diferentes si lee los valores solo una vez dentro del hilo para su uso en la comparación y posterior visualización de los mismos. Cuando hago lo siguiente,
x
siempre se muestra como1
yy
varía entre0
y algunos enteros grandes. Creo que su comportamiento en este punto es algo indefinido sin lavolatile
palabra clave y es posible que la compilación JIT del código contribuya a que actúe así. Además, si comento elsynchronized(this) {}
bloque vacío , el código también funciona y sospecho que es porque el bloqueo causa un retraso suficientecurrentPos
y sus campos se vuelven a leer en lugar de usarse desde el caché.fuente
volatile
.Tiene memoria ordinaria, la referencia 'currentpos' y el objeto Point y sus campos detrás de él, compartidos entre 2 hilos, sin sincronización. Por lo tanto, no hay un orden definido entre las escrituras que le suceden a esta memoria en el hilo principal y las lecturas en el hilo creado (llámelo T).
El hilo principal está haciendo las siguientes escrituras (ignorando la configuración inicial del punto, dará como resultado que px y py tengan valores predeterminados):
Debido a que no hay nada especial sobre estas escrituras en términos de sincronización / barreras, el tiempo de ejecución es libre de permitir que el hilo T los vea ocurrir en cualquier orden (el hilo principal siempre ve escrituras y lecturas ordenadas de acuerdo con el orden del programa), y ocurren en cualquier punto entre las lecturas en T.
Entonces T está haciendo:
Dado que no hay relaciones de orden entre las escrituras en main y las lecturas en T, claramente hay varias formas en que esto puede producir su resultado, ya que T puede ver la escritura de main en currentpos antes de las escrituras en currentpos.y o currentpos.x:
y así sucesivamente ... Hay una serie de carreras de datos aquí.
Sospecho que la suposición defectuosa aquí es pensar que las escrituras que resultan de esta línea se hacen visibles en todos los hilos en el orden del programa del hilo que lo ejecuta:
Java no ofrece tal garantía (sería terrible para el rendimiento). Se debe agregar algo más si su programa necesita un orden garantizado de las escrituras en relación con las lecturas en otros hilos. Otros han sugerido hacer que los campos x, y sean finales, o alternativamente hacer que currentpos sea volátil.
El uso de final tiene la ventaja de que hace que los campos sean inmutables y, por lo tanto, permite que los valores se almacenen en caché. El uso de volátiles conduce a la sincronización en cada escritura y lectura de currentpos, lo que podría afectar el rendimiento.
Consulte el capítulo 17 de las especificaciones del lenguaje Java para ver los detalles sangrientos: http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html
(La respuesta inicial suponía un modelo de memoria más débil, ya que no estaba seguro de que el volátil garantizado JLS fuera suficiente. La respuesta editada para reflejar el comentario de las asilias, señalando que el modelo de Java es más fuerte, sucede antes, es transitivo, y tan volátil en la posición actual también es suficiente )
fuente
currentPos
se hace volátil, la asignación garantiza la publicación segura delcurrentPos
objeto y de sus miembros, incluso si ellos mismos no son volátiles.Point currentPos = new Point(x, y)
, tiene 3 escrituras: (w1)this.x = x
, (w2)this.y = y
y (w3)currentPos = the new point
. El orden del programa garantiza que hb (w1, w3) y hb (w2, w3). Más adelante en el programa que lees (r1)currentPos
. SicurrentPos
no es volátil, no hay hb entre r1 y w1, w2, w3, por lo que r1 podría observar alguno (o ninguno) de ellos. Con volátil, introduce hb (w3, r1). Y la relación hb es transitiva, por lo que también introduce hb (w1, r1) y hb (w2, r1). Esto se resume en la concurrencia de Java en la práctica (3.5.3. Idiomas de publicación segura).Puede usar un objeto para sincronizar las escrituras y las lecturas. De lo contrario, como otros dijeron antes, se producirá una escritura en currentPos en el medio de las dos lecturas p.x + 1 y py
fuente
sem
no se comparte y tratar la declaración sincronizada como no operativa ... El hecho de que resuelva el problema es pura suerte.Está accediendo a currentPos dos veces y no garantiza que no se actualice entre esos dos accesos.
Por ejemplo:
Básicamente estás comparando dos puntos diferentes .
Tenga en cuenta que incluso hacer que CurrentPos sea volátil no lo protegerá de esto, ya que son dos lecturas separadas por el hilo de trabajo.
Agregar un
método a su clase de puntos. Esto asegurará que solo se use un valor de currentPos al verificar x + 1 == y.
fuente