¿Para qué se usan las cercas de memoria en Java?

18

Mientras intentaba entender cómo SubmissionPublisher( código fuente en Java SE 10, OpenJDK | docs ), una nueva clase agregada a Java SE en la versión 9, se implementó, me topé con algunas llamadas a la API VarHandleque no conocía anteriormente:

fullFence` acquireFence` releaseFence, loadLoadFencey storeStoreFence.

Después de investigar un poco, especialmente con respecto al concepto de barreras / vallas de memoria (he oído hablar de ellos anteriormente, sí; pero nunca los usé, por lo que no estaba muy familiarizado con su semántica), creo que tengo una comprensión básica de lo que son . No obstante, dado que mis preguntas pueden surgir de una idea errónea, quiero asegurarme de haber acertado en primer lugar:

  1. Las barreras de memoria son restricciones de reordenamiento con respecto a las operaciones de lectura y escritura.

  2. Las barreras de memoria se pueden clasificar en dos categorías principales: barreras de memoria unidireccionales y bidireccionales, dependiendo de si establecen restricciones en lecturas o escrituras o en ambas.

  3. C ++ admite una variedad de barreras de memoria , sin embargo, estas no coinciden con las proporcionadas por VarHandle. Sin embargo, algunas de las barreras de memoria disponibles VarHandleproporcionan efectos de ordenamiento que son compatibles con sus correspondientes barreras de memoria C ++.

    • #fullFence es compatible con atomic_thread_fence(memory_order_seq_cst)
    • #acquireFence es compatible con atomic_thread_fence(memory_order_acquire)
    • #releaseFence es compatible con atomic_thread_fence(memory_order_release)
    • #loadLoadFencey #storeStoreFenceno tiene contraparte compatible con C ++

La palabra compatible parece ser realmente importante aquí, ya que la semántica difiere claramente en lo que respecta a los detalles. Por ejemplo, todas las barreras de C ++ son bidireccionales, mientras que las barreras de Java no lo son (necesariamente).

  1. La mayoría de las barreras de memoria también tienen efectos de sincronización. Esos dependen especialmente del tipo de barrera utilizada y las instrucciones de barrera ejecutadas previamente en otros hilos. Como todas las implicaciones que tiene una instrucción de barrera son específicas del hardware, me quedaré con las barreras de nivel superior (C ++). En C ++, por ejemplo, los cambios realizados antes de una instrucción de barrera de liberación son visibles para un subproceso que ejecuta una instrucción de barrera de adquisición .

¿Son correctas mis suposiciones? Si es así, mis preguntas resultantes son:

  1. ¿Las barreras de memoria disponibles VarHandlecausan algún tipo de sincronización de memoria?

  2. Independientemente de si causan sincronización de memoria o no, ¿para qué pueden ser útiles las restricciones de reordenamiento en Java? El modelo de memoria de Java ya ofrece algunas garantías muy sólidas con respecto a los pedidos cuando intervienen campos volátiles, bloqueos u VarHandleoperaciones similares #compareAndSet.

En caso de que esté buscando un ejemplo: lo mencionado anteriormente BufferedSubscription, una clase interna de SubmissionPublisher(fuente vinculada anteriormente), estableció una cerca completa en la línea 1079 (función growAndAdd; ya que el sitio web vinculado no admite identificadores de fragmentos, solo CTRL + F para ello ) Sin embargo, no está claro para mí para qué sirve.

Quaffel
fuente
1
He tratado de responder, pero para decirlo de manera muy simple, existen porque la gente quiere un modo más débil que el que tiene Java. En orden ascendente, estos serían: plain -> opaque -> release/acquire -> volatile (sequential consistency).
Eugene

Respuestas:

11

Esto es principalmente una no respuesta, realmente (inicialmente quería hacer un comentario, pero como puede ver, es demasiado largo). Es solo que lo cuestioné mucho, leí e investigué mucho y en este momento puedo decir con seguridad: esto es complicado. Incluso escribí múltiples pruebas con jcstress para descubrir cómo funcionan realmente (mientras observa el código de ensamblaje generado) y aunque algunas de ellas de alguna manera tenían sentido, el tema en general no es en absoluto fácil.

Lo primero que debes entender:

La Especificación del lenguaje Java (JLS) no menciona barreras , en ningún lado. Esto, para Java, sería un detalle de implementación: realmente actúa en términos de suceder antes de la semántica. Para poder especificarlos adecuadamente de acuerdo con el JMM (Modelo de memoria de Java), el JMM tendría que cambiar bastante .

Este es un trabajo en progreso.

En segundo lugar, si realmente quieres rascar la superficie aquí, esto es lo primero que debes ver . La charla es increíble. Mi parte favorita es cuando Herb Sutter levanta sus 5 dedos y dice: "Esta es la cantidad de personas que realmente pueden trabajar correctamente con estos". Eso debería darle una pista de la complejidad involucrada. Sin embargo, hay algunos ejemplos triviales que son fáciles de entender (como un contador actualizado por múltiples subprocesos que no se preocupa por otras garantías de memoria, pero solo se preocupa de que se incremente correctamente).

Otro ejemplo es cuando (en Java) desea que una volatilebandera controle hilos para detener / iniciar. Ya sabes, lo clásico:

volatile boolean stop = false; // on thread writes, one thread reads this    

Si trabaja con Java, sabrá que sin volatile este código está roto (puede leer por qué el bloqueo de verificación doble está roto sin él, por ejemplo). ¿Pero también sabe que para algunas personas que escriben código de alto rendimiento esto es demasiado? volatileleer / escribir también garantiza una coherencia secuencial , que tiene algunas garantías sólidas y algunas personas quieren una versión más débil de esto.

¿Una bandera segura para hilos, pero no volátil? Sí, exactamente: VarHandle::set/getOpaque.

¿Y preguntarías por qué alguien podría necesitar eso, por ejemplo? No todos están interesados ​​en todos los cambios que están respaldados por a volatile.

Veamos cómo lograremos esto en Java. En primer lugar, este tipo de exóticos cosas ya existían en la API: AtomicInteger::lazySet. Esto no está especificado en el modelo de memoria Java y no tiene una definición clara ; Todavía la gente lo usó (LMAX, afaik o esto para leer más ). En mi humilde opinión, AtomicInteger::lazySetes VarHandle::releaseFence(o VarHandle::storeStoreFence).


Intentemos responder por qué alguien los necesita .

JMM tiene básicamente dos formas de acceder a un campo: simple y volátil (que garantiza la coherencia secuencial ). Todos estos métodos que mencionas están ahí para traer algo entre estos dos: semántica de liberación / adquisición ; Hay casos, supongo, donde la gente realmente necesita esto.

Una relajación aún mayor de la liberación / adquisición sería opaca , lo que todavía estoy tratando de entender completamente .


Por lo tanto, en resumen (su comprensión es bastante correcta, por cierto): si planea usar esto en Java, no tienen especificaciones por el momento, hágalo bajo su propio riesgo. Si desea comprenderlos, sus modos equivalentes de C ++ son el lugar para comenzar.

Eugene
fuente
1
No intentes descifrar el significado lazySetal vincular a respuestas antiguas, la documentación actual dice exactamente lo que significa hoy en día. Además, es engañoso decir que JMM solo tiene dos modos de acceso. Tenemos lectura volátil y escritura volátil , que juntas pueden establecer una relación de suceder antes .
Holger
1
Estaba en medio de escribir algo más al respecto. Considere que cas es a la vez, una lectura y una escritura, actuando como una barrera completa, y puede comprender por qué se desea relajarlo. Por ejemplo, cuando se implementa un bloqueo, la primera acción es cas (0, 1) en el recuento de bloqueo, pero solo necesita adquirir semántica (como lectura volátil), mientras que la escritura final de 0 para desbloquear debería tener liberación semántica (como escritura volátil ), por lo que hay un paso previo entre el desbloqueo y el bloqueo posterior. La adquisición / liberación es incluso más débil que la lectura / escritura volátil con respecto a los hilos que utilizan diferentes bloqueos.
Holger
1
@Peter Cordes: la primera versión de C que tenía una volatilepalabra clave fue C99, cinco años después de Java, pero aún carecía de una semántica útil, incluso C ++ 03 no tiene un modelo de memoria. Las cosas que C ++ llama "atómicas" también son mucho más jóvenes que Java. Y la volatilepalabra clave ni siquiera implica actualizaciones atómicas. Entonces, ¿por qué debería llamarse así?
Holger
1
@PeterCordes quizás, lo estoy confundiendo con restrict, sin embargo, recuerdo momentos en los que tuve que escribir __volatilepara usar una extensión del compilador que no es de palabras clave. Entonces, ¿tal vez no implementó C89 por completo? No me digas que soy tan viejo. Antes de Java 5, volatileestaba mucho más cerca de C. Pero Java no tenía MMIO, por lo que su propósito siempre era multihilo, pero la semántica anterior a Java 5 no era muy útil para eso. Por lo tanto, se agregaron lanzar / adquirir como semántica, pero aún así, no es atómico (las actualizaciones atómicas son una característica adicional construida sobre él).
Holger
2
@Eugene con respecto a esto , mi ejemplo fue específico para usar cas para el bloqueo que se adquiriría. Un cierre de cuenta regresiva llevaría decrementos atómicos con la liberación semántica, seguido por el hilo llegando a cero insertando una valla de adquisición y ejecutando la acción final. Por supuesto, hay otros casos de actualizaciones atómicas en los que se requiere toda la cerca.
Holger