Barreras de memoria y estilo de encoding sobre una VM Java

Supongamos que tengo un objeto complejo estático que se actualiza periódicamente por un conjunto de subprocesos y lee más o menos continuamente en un subproceso de larga ejecución. El objeto en sí es siempre inmutable y refleja el estado más reciente de algo.

class Foo() { int a, b; } static Foo theFoo; void updateFoo(int newA, int newB) { f = new Foo(); fa = newA; fb = newB; // HERE theFoo = f; } void readFoo() { Foo f = theFoo; // use f... } 

No me importa en absoluto si mi lector ve el Foo antiguo o el nuevo, sin embargo, necesito ver un objeto completamente inicializado. IIUC, la especificación de Java dice que sin una barrera de memoria en AQUÍ, puedo ver un objeto con fb inicializado pero todavía no comprometido con la memoria. Mi progtwig es un progtwig del mundo real que tarde o temprano comprometerá cosas en la memoria, por lo que no es necesario comprometer el nuevo valor de theFoo en la memoria de inmediato (aunque no haría daño).

¿Cuál crees que es la forma más legible de implementar la barrera de la memoria? Estoy dispuesto a pagar un pequeño precio de rendimiento en aras de la legibilidad si es necesario. Creo que puedo sincronizar la tarea con Foo y eso funcionaría, pero no estoy seguro de que sea muy obvio para alguien que lea el código por qué lo hago. También podría sincronizar toda la inicialización del nuevo Foo, pero eso introduciría más locking que realmente necesitaba.

¿Cómo lo escribirías para que sea lo más legible posible?
Felicitaciones de bonificación por una versión de Scala 🙂

Respuestas cortas a la pregunta original

  • Si Foo es inmutable, simplemente hacer que los campos sean definitivos asegurará la inicialización completa y la visibilidad consistente de los campos en todos los hilos, independientemente de la sincronización.
  • Independientemente de si Foo es inmutable, la publicación a través de volatile theFoo o AtomicReference theFoo es suficiente para garantizar que las escrituras en sus campos sean visibles para cualquier lectura de hilo a través de la referencia de referencia theFoo
  • Usando una asignación simple al theFoo , nunca se garantiza que los hilos del lector vean ninguna actualización
  • En mi opinión, y basado en JCiP, la “forma más legible de implementar la barrera de la memoria” es AtomicReference , con sincronización explícita en segundo lugar, y el uso de volatile en tercer lugar
  • Lamentablemente, no tengo nada para ofrecer en Scala

Puedes usar volatile

Te culpo. Ahora estoy enganchado, he roto JCiP , y ahora me pregunto si algún código que he escrito es correcto. El fragmento de código anterior es, de hecho, potencialmente inconsistente. (Editar: consulte la sección a continuación sobre Publicación segura vía volátil.) El hilo de lectura también podría verse obsoleto (en este caso, cualesquiera que sean los valores predeterminados para a y b ) para el tiempo ilimitado. Puede hacer una de las siguientes acciones para introducir un límite de pase previo:

  • Publicar a través de volatile , lo que crea un borde anterior al equivalente a un monitorenter (lado de lectura) o monitorexit (lado de escritura)
  • Use los campos final e inicialice los valores en un constructor antes de la publicación
  • Introduzca un bloque sincronizado al escribir los nuevos valores en el objeto theFoo
  • Usar campos AtomicInteger

Esto hace que se resuelva el orden de escritura (y resuelve sus problemas de visibilidad). Luego debe abordar la visibilidad de la nueva referencia theFoo . Aquí, volatile es apropiado – JCiP dice en la sección 3.1.4 “Variables volátiles”, (y aquí, la variable es theFoo ):

Puede usar variables volátiles solo cuando se cumplan todos los siguientes criterios:

  • Las escrituras en la variable no dependen de su valor actual, o puede asegurarse de que solo un hilo actualiza el valor;
  • La variable no participa en invariantes con otras variables de estado; y
  • No se requiere locking por ningún otro motivo mientras se accede a la variable

Si haces lo siguiente, eres dorado:

 class Foo { // it turns out these fields may not be final, with the volatile publish, // the values will be seen under the new JMM final int a, b; Foo(final int a; final int b) { this.a = a; this.b=b; } } // without volatile here, separate threads A' calling readFoo() // may never see the new theFoo value, written by thread A static volatile Foo theFoo; void updateFoo(int newA, int newB) { f = new Foo(newA,newB); theFoo = f; } void readFoo() { final Foo f = theFoo; // use f... } 

Directo y legible

Varias personas en este y otros hilos (gracias a Juan V ) señalan que las autoridades sobre estos temas enfatizan la importancia de la documentación del comportamiento y las suposiciones de sincronización. JCiP habla en detalle sobre esto, proporciona un conjunto de anotaciones que pueden usarse para documentación y verificación estática, y también puede consultar el Libro de cocina JMM para ver indicadores sobre comportamientos específicos que requerirían documentación y enlaces a las referencias apropiadas. Doug Lea también preparó una lista de cuestiones a considerar al documentar el comportamiento de simultaneidad . La documentación es adecuada, en particular, debido a la preocupación, el escepticismo y la confusión que rodean los problemas de concurrencia (en SO: “¿El cinismo de concurrencia de Java ha ido demasiado lejos?” ). Además, herramientas como FindBugs ahora proporcionan reglas de comprobación estáticas para detectar violaciones de semántica de anotación JCiP, como “Sincronización inconsistente: IS_FIELD-NOT_GUARDED” .

Hasta que piense que tiene una razón para hacer lo contrario, probablemente sea mejor proceder con la solución más legible, algo como esto (gracias, @Burleigh Bear), usando las anotaciones @Immutable y @GuardedBy .

 @Immutable class Foo { final int a, b; Foo(final int a; final int b) { this.a = a; this.b=b; } } static final Object FooSync theFooSync = new Object(); @GuardedBy("theFooSync"); static Foo theFoo; void updateFoo(final int newA, final int newB) { f = new Foo(newA,newB); synchronized (theFooSync) {theFoo = f;} } void readFoo() { final Foo f; synchronized(theFooSync){f = theFoo;} // use f... } 

o, posiblemente, ya que es más limpio:

 static AtomicReference theFoo; void updateFoo(final int newA, final int newB) { theFoo.set(new Foo(newA,newB)); } void readFoo() { Foo f = theFoo.get(); ... } 

Cuándo es apropiado usar volatile

En primer lugar, tenga en cuenta que esta pregunta se refiere a la pregunta aquí, pero se ha abordado muchas, muchas veces en SO:

  • ¿Cuándo exactamente utilizas volátil?
  • Alguna vez utiliza la palabra clave volátil en Java
  • Para lo que se usa “volátil”
  • Usar palabra clave volátil
  • Java boolean volátil vs. AtomicBoolean

De hecho, una búsqueda en Google: “sitio: stackoverflow.com + java + volátil + palabra clave” devuelve 355 resultados distintos. El uso de volatile es, en el mejor de los casos, una decisión volátil. ¿Cuándo es apropiado? El JCiP brinda algunas pautas abstractas (citadas arriba). Recogeré algunas pautas más prácticas aquí:

  • Me gusta esta respuesta : ” volatile puede usarse para publicar de forma segura objetos inmutables”, que encapsula perfectamente la mayor parte del rango de uso que uno podría esperar de un progtwigdor de aplicaciones.
  • La respuesta de @mdma aquí : “la volatile es más útil en algoritmos sin locking” resume otra clase de usos: algoritmos especiales, sin locking, que son lo suficientemente sensibles al rendimiento para merecer un análisis y validación cuidadosos por parte de un experto.
  • Publicación segura vía volátil

    Siguiendo a @Jed Wesley-Smith , parece que ahora volatile ofrece garantías más sólidas (desde JSR-133), y la afirmación anterior “Puede usar volatile siempre que el objeto publicado sea inmutable” es suficiente pero tal vez no sea necesario.

    En cuanto a las preguntas frecuentes de JMM, las dos entradas ¿Cómo funcionan los campos finales bajo el nuevo JMM? y ¿Qué hace volátil? realmente no se tratan juntos, pero creo que el segundo nos da lo que necesitamos:

    La diferencia es que ahora ya no es tan fácil reordenar los accesos de campo normales a su alrededor. Escribir en un campo volátil tiene el mismo efecto de memoria que una versión de monitor, y la lectura desde un campo volátil tiene el mismo efecto de memoria que la adquisición de un monitor. En efecto, debido a que el nuevo modelo de memoria impone restricciones más estrictas al reordenamiento de los accesos de campo volátiles con otros accesos de campo, volátiles o no, todo lo que era visible para el hilo A cuando se escribe en el campo volátil f se vuelve visible al hilo B cuando lee f.

    Notaré que, a pesar de varias relecturas de JCiP, el texto relevante no saltó hasta que Jed lo señaló. Está en p. 38, sección 3.1.4, y dice más o menos lo mismo que esta cita anterior: el objeto publicado solo tiene que ser efectivamente inmutable, no se requieren campos final , QED.

    Cosas más antiguas, guardadas para rendición de cuentas

    Un comentario: ¿Alguna razón por la cual newA y newB no pueden ser argumentos para el constructor? Entonces puede confiar en las reglas de publicación para los constructores …

    Además, usar una AtomicReference probablemente AtomicReference cualquier incertidumbre (y puede que te compre otros beneficios dependiendo de lo que necesites hacer en el rest de la clase …) Además, alguien más inteligente que yo puede decirte si la volatile resolvería esto, pero siempre me parece críptico …

    En una revisión posterior, creo que el comentario de @Burleigh Bear anterior es correcto — (EDITAR: vea abajo) en realidad no tiene que preocuparse por el orden fuera de secuencia aquí, ya que está publicando un nuevo objeto para theFoo . Mientras que otro subproceso podría concebir valores incoherentes para newA y newB como se describe en JLS 17.11, eso no puede suceder aquí porque se enviarán a la memoria antes de que el otro subproceso conserve una referencia a la nueva instancia de f = new Foo() has creado … esta es una publicación segura y única. Por otro lado, si escribiste

     void updateFoo(int newA, int newB) { f = new Foo(); theFoo = f; fa = newA; fb = newB; } 

    Pero en ese caso los problemas de sincronización son bastante transparentes, y ordenar es la menor de tus preocupaciones. Para obtener orientación útil sobre volátiles, eche un vistazo a este artículo de developerWorks .

    Sin embargo, es posible que haya un problema por el que los subprocesos del lector por separado puedan ver el valor anterior del theFoo durante un tiempo ilimitado. En la práctica, esto rara vez sucede. Sin embargo, se puede permitir que la JVM theFoo en caché el valor de la referencia theFoo en el contexto de otro thread. Estoy bastante seguro de marcar theFoo como volatile abordará esto, al igual que cualquier tipo de sincronizador o AtomicReference .

    Tener un Foo inmutable con los campos a y b resuelve los problemas de visibilidad con los valores predeterminados, pero también lo hace volviendo volátil el Foo.

    Personalmente, me gusta tener clases de valores inmutables de todos modos, ya que son mucho más difíciles de utilizar.