variable sincronizar productores productor procesos hilos ejemplo consumidores consumidor acceso java multithreading findbugs synchronized

java - sincronizar - ¿En qué situaciones podría un bloque sincronizado vacío lograr una semántica de subprocesamiento correcta?



synchronized java ejemplo (5)

Las respuestas anteriores no resaltan lo más útil de los bloques synchronized vacíos: pueden garantizar la visibilidad de los cambios de variables y otras acciones en los subprocesos. Como indica jtahlborn , la sincronización impone una "barrera de memoria" en el compilador que lo obliga a vaciar y actualizar sus cachés. Pero no encontré donde "SnakE discute" esto, así que escribí una respuesta yo mismo.

int variable; void test() // this code is INCORRECT { new Thread( () -> // A { variable = 9; for( ;; ) { // do other stuff } }).start(); new Thread( () -> // B { for( ;; ) { if( variable == 9 ) System.exit( 0 ); } }).start(); }

El programa anterior es incorrecto. El valor de la variable puede almacenarse en caché localmente en el subproceso A o B o ambos. Por lo tanto, B podría nunca leer el valor de 9 que A escribe, y por lo tanto podría hacer un bucle para siempre.

Hacer un cambio de variable visible a través de hilos mediante el uso de bloques synchronized vacíos

Una posible corrección es agregar un modificador volatile (efectivamente "sin caché") a la variable. Sin embargo, a veces esto es ineficiente porque prohíbe totalmente el almacenamiento en caché de la variable. Los bloques synchronized vacíos, por otro lado, no prohíben el almacenamiento en caché. Todo lo que hacen es forzar a los cachés a sincronizarse con la memoria principal en ciertos puntos críticos. Por ejemplo:

int variable; void test() // revised { new Thread( () -> // A { variable = 9; synchronized( o ) {} // flush to main memory for( ;; ) { // do other stuff } }).start(); new Thread( () -> // B { for( ;; ) { synchronized( o ) {} // refresh from main memory if( variable == 9 ) System.exit( 0 ); } }).start(); } final Object o = new Object();

Cómo el modelo de memoria garantiza visibilidad.

Ambos hilos deben sincronizarse en el mismo objeto para garantizar la visibilidad. Esta garantía se basa en el modelo de memoria Java , en particular en la regla de que una "acción de desbloqueo en el monitor m se sincroniza con todas las acciones de bloqueo subsiguientes en m" y, por lo tanto, ocurre antes de esas acciones. Por lo tanto, el desbloqueo del monitor de O en la cola de su bloque synchronized ocurre antes del bloqueo posterior de B en la cabecera de su bloque. (Tenga en cuenta que es este extraño orden de cola de la relación lo que explica por qué los cuerpos pueden estar vacíos). Dado que la escritura de A precede su desbloqueo y la cerradura de B precede a su lectura, la relación debe extenderse para cubrir tanto la escritura como la lectura: escribir sucede-antes de leer . Es esta relación crucial y extendida que hace que el programa revisado sea correcto en términos del modelo de memoria.

Un efecto más profundo que el volátil.

Un bloque synchronized vacío tiene un efecto más profundo que volatile . Supongamos que la variable no es un entero primitivo, como se muestra arriba, sino un objeto mutable compuesto. Supongamos que su contenido se agrega de forma incremental por el subproceso A (un subproceso de trabajo como describe jtahlborn ) para ser leído posteriormente por el subproceso B (consumidor). Luego, declarar la variable volatile ya no será suficiente para corregir el programa, porque el efecto del modificador volatile no se extiende a los contenidos de la variable. Pero agregar bloques synchronized vacíos, como se muestra arriba, todavía lo corregirá.

Creo que estos son los usos más importantes para los bloques synchronized vacíos.

Estaba buscando en un informe de Findbugs en mi base de código y uno de los patrones que se activó fue para un bloque synchronzied vacío (es decir, synchronized (var) {} ). La documentación dice :

Los bloques sincronizados vacíos son mucho más sutiles y difíciles de usar correctamente de lo que la mayoría de las personas reconocen, y los bloques sincronizados vacíos casi nunca son una mejor solución que las soluciones menos sofisticadas.

En mi caso ocurrió porque el contenido del bloque había sido comentado, pero la declaración synchronized todavía estaba allí. ¿En qué situaciones podría un bloque synchronized vacío lograr una semántica de subprocesamiento correcta?


Para ver en profundidad el modelo de memoria de Java, eche un vistazo a este video de la serie "Temas avanzados en lenguajes de programación" de Google: http://www.youtube.com/watch?v=1FX4zco0ziY

Proporciona una visión general muy buena de lo que el compilador puede (a menudo en teoría, pero a veces en la práctica) hacer con su código. Cosas esenciales para cualquier programador serio de Java!


Sincronizar hace un poco más que solo esperar, mientras que la codificación poco elegante puede lograr el efecto requerido.

De http://www.javaperformancetuning.com/news/qotm030.shtml

  1. El hilo adquiere el bloqueo en el monitor para este objeto (suponiendo que el monitor esté desbloqueado, de lo contrario el hilo espera hasta que el monitor se desbloquee).
  2. La memoria de subprocesos borra todas sus variables, es decir, tiene todas sus variables leídas efectivamente desde la memoria "principal" (las JVM pueden usar conjuntos sucios para optimizar esto de modo que solo se vacían las variables "sucias", pero conceptualmente esto es lo mismo. Ver la sección 17.9 de la especificación del lenguaje Java).
  3. Se ejecuta el bloque de código (en este caso, se establece el valor de retorno al valor actual de i3, que puede haberse reiniciado desde la memoria "principal").
  4. (Cualquier cambio en las variables normalmente ahora se escribiría en la memoria "principal", pero para geti3 () no tenemos cambios).
  5. El hilo libera el bloqueo en el monitor para este objeto.

Solía ​​ser el caso que la especificación implicaba ciertas operaciones de barrera de memoria ocurridas. Sin embargo, la especificación ahora ha cambiado y la especificación original nunca se implementó correctamente. Se puede usar para esperar a que otro subproceso libere el bloqueo, pero coordinar que el otro subproceso ya haya adquirido el bloqueo sería complicado.


Un bloque sincronizado vacío esperará hasta que nadie más use ese sincronizador. Puede que sea lo que desee, pero como no ha protegido el código subsiguiente en el bloque sincronizado, nada impide que alguien más modifique lo que estaba esperando mientras ejecutaba el código subsiguiente. Eso es casi nunca lo que quieres.