programacion paso para novatos manuales lenguaje español dummies desde curso cero java concurrency order java-memory-model memory-visibility

paso - ¿Por qué este programa Java termina a pesar de que aparentemente no debería(y no lo hizo)?



lenguaje de programacion java pdf (5)

Obviamente, la escritura en currentPos no ocurre antes de que se lea, pero no veo cómo ese puede ser el problema.

currentPos = new Point(currentPos.x+1, currentPos.y+1); hace algunas cosas, incluida la escritura de valores predeterminados en x e y (0) y luego la escritura de sus valores iniciales en el constructor. Dado que su objeto no se publica de manera segura, las 4 operaciones de escritura pueden ser reordenadas libremente por el compilador / JVM.

Entonces, desde la perspectiva del hilo de lectura, es una ejecución legal leer x con su nuevo valor, pero y con su valor predeterminado de 0, por ejemplo. Cuando llega a la instrucción println (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 a currentPos como volatile garantizará una publicación segura ya que su objeto es efectivamente inmutable: si en su caso de uso real el objeto se muta después de la construcción, volatile garantías volatile no serán suficientes y podría volver a ver un objeto inconsistente.

Alternativamente, puede hacer que el Point inmutable, lo que también garantizará una publicación segura, incluso sin usar volatile . Para lograr la inmutabilidad, simplemente necesita marcar y final.

Como nota al margen y como ya se mencionó, la JVM puede tratar la synchronized(this) {} (no entiendo) (entiendo que la incluyó para reproducir el comportamiento).

Una operación sensible en mi laboratorio de hoy salió completamente mal. Un actuador en un microscopio electrónico sobrepasó su límite, y después de una cadena de eventos perdí $ 12 millones de 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 recibiendo:

$ 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 aritmética de punto 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é?

He notado que esto no sucede en algunos entornos. Estoy en OpenJDK 6 en Linux de 64 bits.


Dado que currentPos se está cambiando fuera del hilo, debe marcarse como volatile :

static volatile Point currentPos = new Point(1,2);

Sin volatilidad, no se garantiza que el subproceso lea las actualizaciones de currentPos que se están realizando en el subproceso principal. Por lo tanto, se siguen escribiendo nuevos valores para currentPos, pero el subproceso continúa utilizando las versiones en caché anteriores por motivos de rendimiento. Dado que solo un hilo 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 usarlos en la comparación y la visualización posterior de ellos. Cuando hago lo siguiente, x siempre se muestra como 1 e y varía entre 0 y algunos enteros grandes. Creo que su comportamiento en este punto es algo indefinido sin la palabra clave volatile y es posible que la compilación JIT del código contribuya a que actúe de esta manera. Además, si comento el bloque synchronized(this) {} vacío synchronized(this) {} , el código también funciona y sospecho que se debe a que el bloqueo causa un retraso suficiente para que currentPos y sus campos se currentPos leer en lugar de usarlos desde el caché.

int x = p.x + 1; int y = p.y; if (x != y) { System.out.println(x+" "+y); System.exit(1); }


Está accediendo a currentPos dos veces y no garantiza que no se actualice entre esos dos accesos.

Por ejemplo:

  1. x = 10, y = 11
  2. hilo de trabajo evalúa px como 10
  3. el hilo principal ejecuta la actualización, ahora x = 11 e y = 12
  4. hilo de trabajo evalúa py como 12
  5. El hilo de trabajo advierte que 10 + 1! = 12, así que imprime y sale.

Básicamente estás comparando dos puntos diferentes .

Tenga en cuenta que incluso hacer volatilidad de currentPos no lo protegerá de esto, ya que son dos lecturas separadas por el subproceso de trabajo.

Añadir un

boolean IsValid() { return x+1 == y; }

Método a su clase de puntos. Esto asegurará que solo se use un valor de currentPos cuando se verifique x + 1 == y.


Podría usar un objeto para sincronizar las escrituras y lecturas. De lo contrario, como han dicho otros antes, se producirá una escritura en currentPos en medio de las dos lecturas p.x + 1 y py

new Thread() { void f(Point p) { 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(); Object sem = new Object(); while (true) { synchronized(sem) { currentPos = new Point(currentPos.x+1, currentPos.y+1); } }


Tiene memoria ordinaria, la referencia de ''currentpos'' y el objeto Point y sus campos detrás, compartidos entre 2 hilos, sin sincronización. Por lo tanto, no hay un orden definido entre las escrituras que pasan a esta memoria en el hilo principal y las lecturas en el hilo creado (llámalo T).

El hilo principal está realizando las siguientes escrituras (ignorando la configuración inicial del punto, resultará en que px y py tengan valores predeterminados):

  • a px
  • para picar
  • a currentpos

Debido a que no hay nada especial acerca de estas escrituras en términos de sincronización / barreras, el tiempo de ejecución es libre de permitir que la hebra T las vea en cualquier orden (la hebra principal, por supuesto, siempre ve las escrituras y las lecturas ordenadas según el orden del programa), y ocurren En cualquier punto entre las lecturas en T.

Así que T está haciendo:

  1. lee currentpos a p
  2. lee px y py (en cualquier orden)
  3. compara y toma la rama
  4. lee px y py (cualquier orden) y llama a System.out.println

Dado que no hay relaciones de orden entre las escrituras en main, y las lecturas en T, hay claramente 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:

  1. Lee currentpos.x primero, antes de que se haya producido la escritura x - obtiene 0, luego lee currentpos.y antes de que haya ocurrido la escritura y - obtiene 0. Compara evals con true. Las escrituras se hacen visibles para T. System.out.println se llama.
  2. Lee currentpos.x primero, después de que haya ocurrido la escritura x, luego lee currentpos.y antes de que haya ocurrido la escritura y - obtiene 0. Compara evals con true. Las escrituras se hacen visibles a T ... etc.
  3. Lee currentpos.y primero, antes de que se haya producido la escritura y (0), luego lee currentpos.x después de la escritura x, evals a verdadero. etc.

y así sucesivamente ... Hay una serie de carreras de datos aquí.

Sospecho que la suposición errónea aquí es pensar que las escrituras que resultan de esta línea se hacen visibles en todos los subprocesos en el orden del programa del subproceso que lo ejecuta:

currentPos = new Point(currentPos.x+1, currentPos.y+1);

Java no ofrece tal garantía (sería terrible para el rendimiento). Se debe agregar algo más si su programa necesita una ordenación garantizada de las escrituras relativas a las lecturas en otros subprocesos. Otros han sugerido hacer que los campos x, y sean finales, o alternativamente hacer que la posición actual sea volátil.

  • Si hace que los campos x, y, sean finales, entonces Java garantiza que las escrituras de sus valores se verán antes de que el constructor regrese, en todos los subprocesos. Por lo tanto, como la asignación a currentpos es posterior al constructor, se garantiza que el subproceso T ve las escrituras en el orden correcto.
  • Si hace que la posición actual sea volátil, entonces Java garantiza que se trata de un punto de sincronización que se ordenará por completo y otros puntos de sincronización. Como en main, las escrituras en x e y deben suceder antes de la escritura en currentpos, entonces cualquier lectura de currentpos en otro hilo debe ver también las escrituras de x, y que sucedieron antes.

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 cables volátiles para la sincronización en cada escritura y lectura de currentpos, lo que podría afectar el rendimiento.

Consulte el capítulo 17 de la especificación del lenguaje Java para obtener información detallada: http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html

(La respuesta inicial asumió un modelo de memoria más débil, ya que no estaba seguro de que el JLS garantizara que la volatilidad fuera suficiente. La respuesta fue editada para reflejar el comentario de las asilias, señalando que el modelo de Java es más fuerte (sucede antes de que sea transitivo), por lo que también es suficiente ).