java volatile java-memory-model

java - Semántica detallada de la volatilidad en cuanto a la puntualidad de la visibilidad.



volatile java-memory-model (6)

Creo que la volatile en Java se expresa en términos de "si ves A, también verás B".

Para ser más explícito, Java promete que cuando el hilo lee una variable volátil foo y ve el valor A, tiene algunas garantías en cuanto a lo que verá cuando lea otras variables más adelante en el mismo hilo. Si el mismo hilo que escribió A to foo también escribió B en la bar (antes de escribir A to foo ), tienes la garantía de ver al menos B en la bar .

Por supuesto, si nunca llegas a ver A, tampoco puedes estar seguro de ver a B. Y si ves B en la bar , eso no dice nada sobre la visibilidad de A en foo . Además, no se garantiza el tiempo que transcurre entre la escritura de subprocesos A hasta foo y otra que ve A en foo .

Considere un volatile int sharedVar . Sabemos que el JLS nos da las siguientes garantías:

  1. cada acción de un hilo de escritura w precediendo su escritura de valor i en sharedVar en el orden del programa happens-before la acción de escritura;
  2. la escritura del valor i por w happens-before la lectura exitosa de i desde sharedVar por un hilo de lectura r ;
  3. la lectura exitosa de i desde sharedVar por el hilo de lectura r happens-before todas las acciones subsiguientes de r en el orden del programa.

Sin embargo, todavía no existe una garantía de tiempo de reloj de pared sobre cuándo el hilo de lectura observará el valor i . Una implementación que simplemente nunca permite que el hilo de lectura vea que el valor sigue cumpliendo con este contrato.

He pensado en esto por un tiempo y no puedo ver ninguna laguna, pero supongo que debe haber. Por favor, señale la laguna en mi razonamiento.


En parte tienes razón. Mi entendimiento es que esto sería legal aunque y solo si el subproceso r no se involucrara en ninguna otra operación que tuviera una relación de suceso antes en relación con el subproceso w .

Así que no hay garantía de cuándo en términos de tiempo de reloj de pared; pero hay una garantía en términos de otros puntos de sincronización dentro del programa.

(Si esto le molesta, considere que, en un sentido más fundamental, no hay garantía de que la JVM ejecutará realmente un código de bytes de manera oportuna. Una JVM que simplemente se paralice para siempre sería casi legal, porque es esencialmente imposible proporcionarla). duros tiempos de garantía en la ejecución.


No es necesario que haya una laguna. De hecho, es teóricamente "legal" implementar una JVM que hizo esto. Del mismo modo, teóricamente es "legal" que nunca se programe un subproceso cuyo nombre comience con una "X" . O implementa una JVM que nunca ejecuta el GC.

Pero en la práctica, las implementaciones de JVM que se comportaron de esta manera no encontrarían ninguna aceptación.

en realidad no lo es, consulte la especificación a la que hice referencia en mi respuesta.

¡Oh sí lo es!

Una implementación que bloquee permanentemente el hilo en la lectura sería técnicamente compatible con JLS 17.4.4. La "lectura subsiguiente" nunca se completa.


Por favor, consulte esta sección (17.4.4) . has torcido un poco la especificación, que es lo que te confunde. La especificación de lectura / escritura para variables volátiles no dice nada sobre valores específicos, específicamente:

  • Una escritura en una variable volátil (§8.3.1.4) v se sincroniza con todas las lecturas subsiguientes de v mediante cualquier subproceso (donde la subsiguiente se define según el orden de sincronización).

ACTUALIZAR:

Como lo menciona @AndrzejDoyle, posiblemente podría haber un hilo r leyendo un valor obsoleto siempre que nada más que el hilo después de ese punto establezca un punto de sincronización con el hilo w en algún punto posterior de la ejecución (ya que entonces usted estaría violando el especulación). Así que sí, hay cierto margen de maniobra allí, pero el subproceso r sería muy restringido en lo que podría hacer (por ejemplo, escribir en System.out establecería un punto de sincronización posterior, ya que la mayoría de los implementos de flujo están sincronizados).


Resulta que las respuestas y las discusiones subsiguientes solo consolidaron mi razonamiento original. Ahora tengo algo en el camino de una prueba:

  1. tomar el caso donde el hilo de lectura se ejecuta en su totalidad antes de que el hilo de escritura comience a ejecutarse;
  2. observe el orden de sincronización que esta ejecución particular creó;
  3. ahora cambie los hilos en el reloj de pared para que se ejecuten en paralelo, pero mantengan el mismo orden de sincronización .

Dado que el modelo de memoria Java no hace referencia al tiempo de reloj de pared, no habrá obstáculos para esto. Ahora tiene dos subprocesos ejecutándose en paralelo con el subproceso de lectura sin observar acciones realizadas por el subproceso de escritura . QED.

Ejemplo 1: Una escritura, un hilo de lectura

Para hacer que este hallazgo sea sumamente conmovedor y real, considere el siguiente programa:

static volatile int sharedVar; public static void main(String[] args) throws Exception { final long startTime = System.currentTimeMillis(); final long[] aTimes = new long[5], bTimes = new long[5]; final Thread a = new Thread() { public void run() { for (int i = 0; i < 5; i++) { sharedVar = 1; aTimes[i] = System.currentTimeMillis()-startTime; briefPause(); } }}, b = new Thread() { public void run() { for (int i = 0; i < 5; i++) { bTimes[i] = sharedVar == 0? System.currentTimeMillis()-startTime : -1; briefPause(); } }}; a.start(); b.start(); a.join(); b.join(); System.out.println("Thread A wrote 1 at: " + Arrays.toString(aTimes)); System.out.println("Thread B read 0 at: " + Arrays.toString(bTimes)); } static void briefPause() { try { Thread.sleep(3); } catch (InterruptedException e) {throw new RuntimeException(e);} }

En lo que respecta a JLS, este es un resultado legal:

Thread A wrote 1 at: [0, 2, 5, 7, 9] Thread B read 0 at: [0, 2, 5, 7, 9]

Tenga en cuenta que no confío en ningún informe defectuoso por currentTimeMillis . Los tiempos reportados son reales. Sin embargo, la implementación eligió hacer que todas las acciones del hilo de escritura sean visibles solo después de todas las acciones del hilo de lectura.

Ejemplo 2: Dos hilos de lectura y escritura.

Ahora @StephenC argumenta, y muchos estarían de acuerdo con él, que eso sucede: antes , aunque no lo mencione explícitamente, todavía implica un orden de tiempo. Por lo tanto, presento mi segundo programa que demuestra la medida exacta en que esto puede ser así.

public static void main(String[] args) throws Exception { final long startTime = System.currentTimeMillis(); final long[] aTimes = new long[5], bTimes = new long[5]; final int[] aVals = new int[5], bVals = new int[5]; final Thread a = new Thread() { public void run() { for (int i = 0; i < 5; i++) { aVals[i] = sharedVar++; aTimes[i] = System.currentTimeMillis()-startTime; briefPause(); } }}, b = new Thread() { public void run() { for (int i = 0; i < 5; i++) { bVals[i] = sharedVar++; bTimes[i] = System.currentTimeMillis()-startTime; briefPause(); } }}; a.start(); b.start(); a.join(); b.join(); System.out.format("Thread A read %s at %s/n", Arrays.toString(aVals), Arrays.toString(aTimes)); System.out.format("Thread B read %s at %s/n", Arrays.toString(bVals), Arrays.toString(bTimes)); }

Solo para ayudar a entender el código, este sería un resultado típico del mundo real:

Thread A read [0, 2, 3, 6, 8] at [1, 4, 8, 11, 14] Thread B read [1, 2, 4, 5, 7] at [1, 4, 8, 11, 14]

Por otro lado, nunca esperaría ver algo como esto, pero sigue siendo legítimo según los estándares de la JMM :

Thread A read [0, 1, 2, 3, 4] at [1, 4, 8, 11, 14] Thread B read [5, 6, 7, 8, 9] at [1, 4, 8, 11, 14]

La JVM en realidad tendría que predecir lo que el Subproceso A escribirá en el momento 14 para saber qué permitir que el Subproceso B lea en el momento 1. La verosimilitud e incluso la viabilidad de esto es bastante dudosa.

De esto podemos definir la siguiente libertad realista que puede tomar una implementación de JVM:

La visibilidad de cualquier secuencia ininterrumpida de acciones de liberación por un hilo puede posponerse de forma segura hasta antes de la acción de adquisición que lo interrumpe.

Los términos liberación y adquisición se definen en JLS §17.4.4 .

Un corolario a esta regla es que las acciones de un hilo que solo escribe y nunca lee nada se puede posponer indefinidamente sin violar la relación de suceso-antes .

Aclarando el concepto volátil

El modificador volatile es en realidad sobre dos conceptos distintos:

  1. La dura garantía de que las acciones en él respetarán lo que sucede antes de ordenar;
  2. La suave promesa del mejor esfuerzo de un runtime hacia una publicación oportuna de escritos.

Tenga en cuenta que el punto 2. no está especificado por el JLS de ninguna manera, simplemente surge por una expectativa general. Una implementación que rompe la promesa sigue siendo obediente, obviamente. Con el tiempo, a medida que avanzamos hacia arquitecturas paralelas masivas, esa promesa puede resultar bastante flexible. Por lo tanto, espero que en el futuro la combinación de la garantía con la promesa sea insuficiente: dependiendo del requisito, necesitaremos una sin la otra, una con un sabor diferente de la otra, o cualquier número de otras combinaciones.


Ya no creo nada de lo de abajo. Todo se reduce al significado de "subsecuente", que no está definido, excepto por dos menciones en 17.4.4, donde está tautológicamente "definido de acuerdo con el orden de sincronización".

Lo único que realmente tenemos que seguir es en la sección 17.4.3:

La consistencia secuencial es una garantía muy fuerte que se hace sobre la visibilidad y el ordenamiento en una ejecución de un programa. Dentro de una ejecución secuencialmente consistente, hay un orden total sobre todas las acciones individuales (como lecturas y escrituras) que es consistente con el orden del programa, y ​​cada acción individual es atómica y es inmediatamente visible para cada hilo. (énfasis añadido)

Creo que existe una garantía de tiempo real, pero hay que armarla de varias secciones del capítulo 17 de JLS .

  1. Según la sección 17.4.5, "la relación de suceso-antes define cuándo tienen lugar las carreras de datos". No parece expresarse explícitamente, pero supongo que esto significa que si ocurre una acción - antes de otra acción a '' , no hay una carrera de datos entre ellos.
  2. De acuerdo con 17.4.3: "Un conjunto de acciones es secuencialmente consistente si ... cada lectura r de una variable v ve el valor escrito por la escritura w a v tal que w aparece antes que r en el orden de ejecución ... Si a el programa no tiene carreras de datos, entonces todas las ejecuciones del programa parecerán ser consistentemente secuenciales ".

Si escribe en una variable volátil v y luego la lee en otro hilo, eso significa que las escrituras ocurren antes de la lectura. Eso significa que no hay una carrera de datos entre la escritura y la lectura, lo que significa que deben ser secuencialmente coherentes. Eso significa que la lectura r debe ver el valor escrito por la escritura w (o una escritura posterior).