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 VarHandle
que no conocía anteriormente:
fullFence
` acquireFence
` releaseFence
, loadLoadFence
y 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:
Las barreras de memoria son restricciones de reordenamiento con respecto a las operaciones de lectura y escritura.
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.
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 disponiblesVarHandle
proporcionan efectos de ordenamiento que son compatibles con sus correspondientes barreras de memoria C ++.#fullFence
es compatible conatomic_thread_fence(memory_order_seq_cst)
#acquireFence
es compatible conatomic_thread_fence(memory_order_acquire)
#releaseFence
es compatible conatomic_thread_fence(memory_order_release)
#loadLoadFence
y#storeStoreFence
no 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).
- 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:
¿Las barreras de memoria disponibles
VarHandle
causan algún tipo de sincronización de memoria?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
VarHandle
operaciones 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.
plain -> opaque -> release/acquire -> volatile (sequential consistency)
.Respuestas:
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:
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
volatile
bandera controle hilos para detener / iniciar. Ya sabes, lo clásico: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?volatile
leer / 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.¿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::lazySet
esVarHandle::releaseFence
(oVarHandle::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.
fuente
lazySet
al 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 .volatile
palabra 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 lavolatile
palabra clave ni siquiera implica actualizaciones atómicas. Entonces, ¿por qué debería llamarse así?restrict
, sin embargo, recuerdo momentos en los que tuve que escribir__volatile
para 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,volatile
estaba 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).