W3docs

La palabra clave volatile en Java

Usa volatile en Java para garantizar visibilidad y ordenamiento de lecturas y escrituras de campos entre hilos, sin necesidad de bloqueos.

synchronized resuelve dos problemas a la vez: exclusión mutua y visibilidad de memoria. A veces solo necesitas lo segundo. Una simple bandera booleana que un hilo establece y otro hilo consulta no necesita acceso exclusivo — hay un único escritor y uno o varios lectores, y el trabajo en sí no se compone. Bloquearla es excesivo; no bloquearla está roto. volatile es la palabra clave que te da visibilidad sin exclusión.

Es una herramienta específica. Úsala correctamente y es la primitiva segura para hilos más rápida que tiene Java. Úsala mal — intentando usarla para actualizaciones compuestas como ++ — y tendrás los mismos errores que synchronized debía corregir.

El problema que volatile existe para resolver

Sin ninguna sincronización:

class Worker implements Runnable {
  boolean stop = false;                                 // plain field
  public void run() {
    while (!stop) {                                     // hot loop
      doWork();
    }
  }
}

Worker w = new Worker();
new Thread(w).start();
Thread.sleep(1000);
w.stop = true;                                          // ask it to stop

Este programa puede no detenerse nunca. La razón: nada en la JVM está obligado a vaciar stop de la caché de CPU de un hilo a la memoria principal, y nada está obligado a invalidar la copia en caché del trabajador cuando el hilo principal escribe. El JIT también puede sacar !stop fuera del bucle por completo — el compilador ve que el cuerpo no modifica stop, así que almacena el valor en un registro. Para siempre.

Este es el error de visibilidad. La solución:

volatile boolean stop = false;

Ahora toda escritura en stop se vacía a la memoria principal y toda lectura proviene de la memoria principal. El JIT no puede sacar la lectura del bucle; el bucle ve el nuevo valor en microsegundos tras la actualización del escritor.

Lo que volatile garantiza

Cuatro cosas:

  1. Visibilidad. Se garantiza que una escritura en un campo volatile sea visible para lecturas posteriores en cualquier otro hilo.
  2. Atomicidad de lectura y escritura — pero solo para el campo en sí. La lectura/escritura de un volatile long y volatile double es atómica; sin volatile, una lectura/escritura de 64 bits puede fragmentarse en una JVM de 32 bits.
  3. Ordenamiento (happens-before). Todo lo que sucedió antes de una escritura volatile en el hilo escritor es visible para todo lo que sucede después de la lectura volatile correspondiente en el hilo lector. Esta es la parte que no ves en la sintaxis pero que es la garantía más poderosa.
  4. Sin reordenamiento en torno al acceso. El compilador y la CPU no pueden mover lecturas/escrituras ordinarias más allá de un acceso volatile en ninguna dirección.

La tercera — happens-before mediante un par de escritura/lectura volatile — a veces se llama la primitiva de publicación más barata. Si escribes un campo, y luego un volatile boolean ready = true, otro hilo que vea ready == true tiene garantizado que también verá la escritura anterior. Así se hace mucha inicialización segura para hilos sin un bloqueo.

Lo que volatile no garantiza

El error más común:

volatile int counter = 0;

void increment() { counter++; }                       // STILL BROKEN

volatile hace que la lectura sea atómica y la escritura sea atómica — pero counter++ es leer-modificar-escribir, lo que son tres operaciones. Dos hilos leen 42, ambos calculan 43, ambos escriben 43. Un incremento sigue perdiéndose.

Para actualizaciones compuestas, volatile no es suficiente. Usa synchronized, un Lock, o — casi siempre la respuesta correcta — un AtomicInteger.

Lo otro que volatile no hace:

  • No proporciona exclusión mutua. Dos hilos pueden escribir en un campo volatile simultáneamente; el resultado es "uno gana, el otro se pierde," que es la respuesta correcta para el estado tipo bandera y la respuesta incorrecta para "combinar estos dos valores."
  • No sincroniza múltiples campos atómicamente juntos. Si tu invariante es "si A es verdadero, entonces B debe ser 42," escribir cada uno en un campo volatile separado puede dejar a otro hilo viendo el nuevo A y el antiguo B.

El patrón de publicación

La aplicación más útil de volatile fuera del caso de la bandera de parada:

class LazyResource {
  private Resource cached;                            // not volatile
  private volatile boolean ready = false;             // the publication flag

  public Resource get() {
    if (!ready) {
      synchronized (this) {
        if (!ready) {
          cached = buildResource();                   // expensive init
          ready = true;                               // publishes cached
        }
      }
    }
    return cached;
  }
}

La escritura volatile de ready = true publica la escritura anterior en cached. Cualquier hilo que luego lea ready == true tiene garantizado ver el cached completamente inicializado. El bloqueo solo es disputado en la primera llamada; las llamadas posteriores solo leen ready y omiten la sincronización por completo. Este es el idioma clásico de double-checked locking, hecho correcto mediante volatile.

Sin volatile en ready, la optimización está rota — el segundo hilo puede ver ready == true y luego leer cached como null. Con volatile, no puede.

(La alternativa moderna es simplemente hacer private final Resource cached = buildResource(); si la inicialización anticipada es aceptable, o Supplier<Resource> lazy = Suppliers.memoize(...) de la biblioteca de tu elección. El double-checked locking manual es raro en el código moderno; el patrón vale la pena conocerlo porque lo leerás.)

Cuándo volatile es exactamente correcto

Un pequeño conjunto de casos de uso donde volatile es la mejor respuesta:

  • Una bandera de estado con un único escritor leída por muchos hilos. Señales de parada, banderas "ready", números de versión de configuración.
  • Una referencia a un valor inmutable que se intercambia atómicamente. Caché que el escritor reconstruye y publica; los lectores ven el objeto completo antiguo o nuevo, nunca uno a medio construir.
  • Publicar el resultado de una inicialización única mediante el patrón double-checked-locking.
  • Una marca de tiempo o número de secuencia que un hilo establece y otros leen — donde perder la actualización más reciente es aceptable pero el valor siempre debe ser autoconsistente.

Fuera de esos casos, normalmente quieres una herramienta de nivel superior. No seas ingenioso con volatile — su semántica es demasiado delgada para soportar el estado general.

volatile long y volatile double en JVMs de 32 bits

Un matiz histórico. En una JVM de 32 bits, una lectura/escritura de long o double no volatile puede dividirse en dos accesos de 32 bits. Dos hilos pueden producir un valor fragmentado — los 32 bits altos de una escritura y los 32 bits bajos de otra. volatile long y volatile double están garantizados como atómicos independientemente del tamaño de la palabra.

Las JVMs modernas casi siempre funcionan en 64 bits, y las JVMs de 64 bits hacen que todos los primitivos sean atómicos de todos modos, pero la regla todavía está en la especificación. Si tienes un long/double compartido entre hilos, márcalo como volatile (o usa AtomicLong/AtomicDouble).

Un ejemplo trabajado: el error de la bandera de parada

El programa a continuación ejecuta el trabajador con un boolean simple primero (que generalmente nunca se detiene), luego con un boolean volatile (que se detiene rápidamente). Un watchdog corta el caso roto después de 2 segundos para que la demo termine.

java— editable, runs on the server

Lo que se puede extraer de la ejecución:

  • El trabajador PLAIN a menudo no se detuvo dentro de la ventana del watchdog. El hilo principal escribió stop = true; el hilo trabajador, con su copia de stop almacenada en un registro, nunca lo notó. Agrega volatile y el error desaparece. Este es el problema de visibilidad — todo programa multihilo tiene una versión de él.
  • El trabajador VOLATILE se detuvo en un microsegundo o dos tras la escritura. Ese es el costo de la barrera de memoria que emite la JVM para un par de escritura/lectura volatile — unos pocos microsegundos en el peor caso, y a menudo menor. Barato para el caso de uso correcto.
  • El contador VOLATILE++ fue menor que 200,000. volatile no convierte n++ en una operación atómica — sigue siendo leer-modificar-escribir, y dos hilos pueden leer 42 y ambos almacenar 43. Este es el uso incorrecto más común de volatile. Para actualizaciones compuestas, usa AtomicInteger (siguiente capítulo).
  • El costo de volatile es pequeño pero real — cada lectura y cada escritura toca la memoria principal y emite una barrera de memoria. Para una bandera que se lee una vez por iteración del bucle, es insignificante. Para un campo que se lee en un bucle interno compacto, el costo de la barrera se acumula. Si encuentras un volatile activo en un perfil, considera si la lectura puede sacarse a una variable local y el cuerpo del bucle ejecutarse en ese snapshot.
  • volatile es también el mecanismo de publicación para una referencia — escribe los datos, luego haz una escritura volatile de la referencia. El hilo lector que ve la nueva referencia tiene garantizado ver todos los datos que el escritor preparó. Ese es el bloque de construcción de los patrones double-checked-locking e intercambio de valores inmutables.

Qué sigue

El siguiente capítulo, Variables atómicas en Java, presenta AtomicInteger, AtomicLong, AtomicReference, y el resto de la familia java.util.concurrent.atomic — la herramienta correcta para "incrementar un contador desde muchos hilos" y cualquier otra actualización compuesta sin bloqueos.

Práctica

Práctica
Declaras `private volatile int counter = 0;` y llamas a `counter++` desde múltiples hilos. Después de que 4 hilos hagan cada uno 100,000 incrementos, ¿qué valor tiene típicamente `counter`?
Declaras `private volatile int counter = 0;` y llamas a `counter++` desde múltiples hilos. Después de que 4 hilos hagan cada uno 100,000 incrementos, ¿qué valor tiene típicamente `counter`?
Was this page helpful?