thread method example blocks java multithreading java-memory-model

java - method - ¿Por qué `sincronizado(nuevo Object()){}` no funciona?



synchronized object java (2)

El siguiente patrón, que algunas personas usan para forzar una barrera de memoria, no funciona:

No se garantiza que sea un no-op, pero la especificación permite que sea un no-op. La especificación solo requiere sincronización para establecer una relación de suceso anterior entre dos subprocesos cuando los dos subprocesos se sincronizan en el mismo objeto, pero en realidad sería más fácil implementar una JVM donde la identidad del objeto no importara.

Pensé que la declaración sincronizada hará que los cachés se vacíen

No hay "caché" en la Especificación del lenguaje Java. Ese es un concepto que solo existe en los detalles de algunas (bueno, OK, prácticamente todas) las plataformas de hardware y las implementaciones de JVM.

En el siguiente código:

class A { private int number; public void a() { number = 5; } public void b() { while(number == 0) { // ... } } }

Si se llama al método b y luego se inicia un nuevo subproceso que activa el método a, entonces no se garantiza que el método b vea nunca el cambio de number y, por lo tanto, b nunca puede terminar.

Por supuesto, podríamos hacer que el number volatile para resolver esto. Sin embargo, por razones académicas, supongamos que volatile no es una opción:

Las preguntas frecuentes de JSR-133 nos dicen:

Después de salir de un bloque sincronizado, liberamos el monitor, que tiene el efecto de vaciar el caché a la memoria principal , para que las escrituras realizadas por este hilo puedan ser visibles para otros hilos. Antes de que podamos ingresar a un bloque sincronizado, adquirimos el monitor, que tiene el efecto de invalidar la memoria caché del procesador local para que las variables se vuelvan a cargar desde la memoria principal.

Esto parece que solo necesito tanto a como b para entrar y salir de cualquier bloqueo synchronized , sin importar qué monitor usen. Más precisamente suena así ...:

class A { private int number; public void a() { number = 5; synchronized(new Object()) {} } public void b() { while(number == 0) { // ... synchronized(new Object()) {} } } }

... eliminaría el problema y garantizará que b verá el cambio en a y, por lo tanto, también terminará eventualmente.

Sin embargo, las preguntas frecuentes también establecen claramente:

Otra implicación es que el siguiente patrón, que algunas personas usan para forzar una barrera de memoria, no funciona:

synchronized (new Object()) {}

Esto es en realidad un no-op, y su compilador puede eliminarlo por completo, porque el compilador sabe que ningún otro hilo se sincronizará en el mismo monitor. Debe configurar una relación de suceso anterior para que un hilo vea los resultados de otro.

Ahora eso es confuso. Pensé que la declaración sincronizada hará que los cachés se vacíen. Seguramente no puede vaciar un caché a la memoria principal de manera que los cambios en la memoria principal solo se puedan ver mediante subprocesos que se sincronizan en el mismo monitor, especialmente porque para volátiles que básicamente hacen lo mismo, ni siquiera necesitamos un monitor, o me equivoco allí? Entonces, ¿por qué esto es un no-op y no hace que b finalice por garantía?


Las preguntas frecuentes no son la autoridad al respecto; el JLS es La Sección 17.4.4 especifica las relaciones sincronizadas con las que se suceden en las relaciones antes de pasar (17.4.5). La viñeta relevante es:

  • Una acción de desbloqueo en el monitor m se sincroniza con todas las acciones de bloqueo posteriores en m (donde "subsecuente" se define de acuerdo con el orden de sincronización).

Como m aquí es la referencia al new Object() , y nunca se almacena ni publica en ningún otro hilo, podemos estar seguros de que ningún otro hilo adquirirá un bloqueo en m después de que se libere el bloqueo en este bloque. Además, dado que m es un objeto nuevo, podemos estar seguros de que no hay ninguna acción que previamente se haya desbloqueado en él. Por lo tanto, podemos estar seguros de que ninguna acción se sincroniza formalmente con esta acción.

Técnicamente, ni siquiera necesita hacer un vaciado completo de caché para cumplir con las especificaciones JLS; Es más de lo que JLS requiere. Una implementación típica hace eso, porque es lo más fácil que el hardware le permite hacer, pero va "más allá", por así decirlo. En los casos en que el análisis de escape le dice a un compilador optimizador que necesitamos aún menos, el compilador puede realizar menos. En su ejemplo, el análisis de escape puede decirle al compilador que la acción no tiene ningún efecto (debido al razonamiento anterior) y puede optimizarse por completo.