Volatile, o como evitar usar bloqueos en concurrencia

En este articulo voy a explicar que es, para que sirve y cuando y como usar la palabra reservada “volatile“.

Qué es volatile

Creo que puedo resumirlo en lo siguiente: volatile es la forma adecuada de publicar un objeto inmutable sin usar mecanismos de bloqueo.

¿Por qué se necesita usar?

Porque la JVM hace lo que le viene en gana con una variable compartida y un cambio en una variable compartida por un hilo puede ser visto por otros hilos o no.

Esto es debido a que, por optimización, la JVM puede hacer cache de la variable. O no hacerlo.

Si hace cache, el cambio de un hilo en una variable compartida no sera visto por otros hilos. Cada hilo tendrá su copia local de la variable.

En principio, la JVM hace la copia local cuando intuye que la variable compartida no va a ser modificada. Para evitarlo, podemos usar la palabra reservada volatile, para indicar que esa variable es modificable, que NO haga una copia local. Si queremos indicar lo contrario y forzar la copia local (cada hilo tenga siempre su copia de la variable compartida y que el resto de hilos no vean los cambios que otros hilos hagan en ella), usaremos el antónimo, threadlocal, con lo que la variable será exclusiva de cada hilo.

La concurrencia siempre dando problemas…

Si, así es. Tenemos que manejar dos conceptos para no tener problemas cuando tenemos concurrencia:

  • Asegurar la exclusión mutua, que significa asegurar que solo un hilo tiene acceso a una variable compartida en un momento dado.
  • Asegurar la visibilidad, que significa asegurar que un cambio en una variable compartida realizado por un hilo es visible al resto de hilo.

La forma típica de asegurar estas dos características es establecer una región crítica (bloque de instrucciones delicado) mediante un bloqueo de acceso a la misma controlado con un monitor o testigo que solo puede ser poseído por un hilo cada vez, que pasa a ser el único hilo capaz de ejecutar esa región critica (ahora recuerdo que todo esto es parte de una serie de artículos sobre concurrencia que aún no he pasado al blog…)

La palabra mágica, volatile

volatile tiene que ver con el segundo concepto que necesitamos manejar en concurrencia, la visibilidad. Es un indicador para el compilador y los hilos para que no hagan copia local de una variable compartida y siempre lean su valor de la memoria principal.

Pero como siempre en concurrencia, hay que tener mucho cuidado. Solo implica eso. No implica en ningún caso atomicidad en las asignaciones realizadas en la variable.

Las variables volatile comparten la característica de visibilidad de synchronized, pero ninguna de las características de atomicidad.

Entonces, porqué usar volatile en vez de un bloqueo

Pues por dos razones: simplicidad y escalabilidad (volatile no puede causar un bloqueo de un hilo).

Un bloqueo nos da visibilidad de cambios pero es muy pesado. Es serializar nuestro programa diseñado para correr en hilos paralelos. volatile no implica un bloqueo.

Veamos algo simple:

boolean parar=false;

//....unas lineas de código mas tarde....

while (!parar) {/*algo*/}

Supongamos varios hilos ejecutando un código parecido a ese. En un momento dado queremos para la ejecución cambiando la variable compartida parar a true. Esto se realiza desde otro hilo:

//hilo de control
parar = true;

Si no hacemos nada, existen posibilidades de que ese cambio no sea visto por el resto de hilos y estos no paren su ejecución.

Si usamos un bloqueo en while conseguimos ver el cambio pero serializamos todo nuestro programa en ese punto. Con volatile no existe bloqueo, solo aseguramos que están leyendo la misma variable.

Vale, ¿eso es todo? ¿Hemos terminado?

No, no iba a ser tan simple. Otra característica del volatile es que establece una relación happens-before entre la escritura de la variable y su lectura.

Wikipedia explica mejor que yo que es una relación happens-before:

In Java specifically, a happens-before relationship is a guarantee that memory written to by statement A is visible to statement B, that is, that statement A completes its write before statement B starts its read.

Para ello, uno de las efectos es evitar reordenar el código. El compilador tiene la potestad de reordenar el código para conseguir una ejecución mas rápida del mismo pero en concurrencia existen condiciones de carrera que hace que esto provoque un desastre.

Por ejemplo:

final class Publisher {
 public static Publisher published;
 int num;

 Publisher(int number) {
   // Initialization
   this.num = number;
   // ...
   published = this;
   }
}

En ese código, el compilador puede reordenar y publicar el this antes de construirlo. Para evitarlo solo tenemos que añadir volatile a la declaración de la variable published.

Veamos otro ejemplo, este de stackoverflow, teniendo esta clase:

public class SO {
  private volatile String a;
  private volatile String b;

  public SO() {
    a = null;
    b = null;
  }

  public void setBothNonNull( @NotNullfinalString one, @NotNullfinalString two ) {
    a = one;
    b = two;
  }

  public String getA() {
    return a;
  }
  public String getB() {
    return b;
  }
}

En este código se asigna primero a y despues se asigna b. Si hacemos lo siguiente:

doIt() {
    if ( so.getB() != null ) {
        System.out.println( so.getA().length );
}

Nunca debería lanza un null pointer, pero eso solo se asegura por que hemos puesto volatile en la declaración de las variable. Sin el volatile, el compilador podría reordenar y, si se ejecutan a la vez doIt y setBothNonNull, doIt podría encontrar asignada solo b.

Aún hay más, efectos en otras variables…

Si tenemos una variable volatile, por ejemplo, pepe, si un hilo escribe en pepe, los valores de las variables previas a pepe que eran visibles a para ese hilo, también será visibles para el resto de los hilos. Así en este ejemplo:

public class Test {
    volatile static private int a;
    static private int b;
    public static void main(String [] args) throws Exception {
        for (int i = 0; i < 100; i++) {
            new Thread() {
                @Override
                public void run() {
                    while (a==0) {
                    }
                    if (b == 0) {
                        System.out.println("error");
                    }
                }
            }.start();
        }
        b = 1;
        a = 1;
    }
}

nunca debería ocurrir que se imprimiese “error” porque cuando en el hilo principal se pone a = 1, ya hemos puesto b = 1 y gracias a volatile, este valor siempre será visible al resto de hilos. (en realidad hay un bug, arreglado a partir de la Java 7u6 build b14 que permite que pueda ocurrir, ver stackoverflow)

Que es lo que no aporta volatile

Como he indicado antes, no implica atomicidad. No es suficiente para implementar un contador (x++ no es atómico).

Podemos usar el ejemplo de la cuenta bancaria:

public class BankAccount {

    private volatile int value;

    public int getValue() { return value; }

    public int increment() {return value++;}
}

Si hacemos que la variable sea volatile, en teoría, como otros hilos ven el incremento, debería funcionar. En la practica, el cambio lo ven, pero el “++” son varias operaciones y mientras se realizan, se “cuelan” otros hilos y cambian el valor antes de que se realice la asignación final.

Podría ser valido si solo un hilo escribe en la variable, por lo que podríamos estar tentados de usar la chollo-sincronizaciión:

public class CheesyCounter {
    private volatile int value;
    public int getValue() { return value; }

    public synchronized int increment() {
        return value++;
    }
}

Funciona, pero es mejor usar un read-write lock (un día de esto escribiré el articulo en el que cuento esto).

En general, volatile no puede usarse cuando un nuevo valor depende de uno antiguo ni tampoco cuando el valor de una variable depende del valor de otra variable.

Como podemos usar volatile

Status flag

Lo hemos visto antes, el típico “sigue hasta que te diga que pares”:

....
volatile boolean shutdownRequested; 
...
public void shutdown() { 
    shutdownRequested = true; 
} 

public void doWork() { 
    while (!shutdownRequested) {
        // do stuff 
    } 
}

Simple, ¿verdad?

Veamos algo un poco mas delicado:

public class UserManager {
    public volatile String lastUser;

    public boolean authenticate(String user, String password) {
        boolean valid = passwordIsValid(user, password);
        if (valid) {
            User u = new User();
            activeUsers.add(u);
            lastUser = user;
        }
        return valid;
    }
}

En este pequeño ejemplo hay una pequeña maldad escondida. Podemos publicar sin problema quien es el ultimo usuario cambiando el valor de lastUser cada vez que se autentica un nuevo usuario y todos los hilos verán siempre el ultimo usuario. Ahora bien, si un hilo hace lastUser.setUser(“nuevo_nombre”), el resto de los hilos no es seguro que vean ese cambio.

Repito: volatile es la forma adecuada de publicar un objeto inmutable sin usar mecanismos de bloqueo.

Mas usos

  • Leer y escribir variables long y double de forma atómica.

Son variables de 64 bits y la lectura de escritura de forma atómica es dependiente de la plataforma. En algunos casos se realiza en dos pasos, escribiendo 32 bits cada vez, por lo que en concurrencia puede provocar errores, al ser posible que un hilo vea un valor intermedio. Usando volátile la escritura y lectura de variables es atómica en java.

  • double checked locking en Singleton
    public class Singleton{
        private static volatile Singleton _instance; //volatile variable
        public static Singleton getInstance(){
        if(_instance == null){
            synchronized(Singleton.class){
                if(_instance == null) _instance = new Singleton();
            }
        }
        return _instance;
    }

Puede ocurrir que entren dos hilos, uno llega al new y cambia el valor de _instance pero el otro hilo no lo ve, entra en el sincro y hace otro new.

Además, existe el riesgo de ver una referencia actualizada pero que apunte a un construido parcialmente.

Con volatile asegurarnos la visibilidad de ese cambio.

No obstante, el Singleton se implementa mejor con un enum o un static factory method.

  • Evitar que otros hilos vea objetos parcialmente construidos si estos objetos son inmutables o thread-safe
    public class Cargador {
        public volatile ObjetoInmutable inmutable;
    
        public void initInBackground() {
            inmutable = new ObjetoInmutable();  
        }
    }
    
    public class otraClase{
        public void doWork() {
            while (true) { 
                ...
               if (cargador.inmutable != null) 
                    metodoxxx(cargador.inmutable);
            }
        }
    }

    Si la referencia inmutable no fuese volatile, doWork() podría estar viendo un objeto ObjetoInmutable parcialmente construido.  Requiere que el objeto sea inmutable o thread-safe.

Y eso es todo?

Si, básicamente aquí acaba este articulo. Se pueden buscar mas patas al gato, pero no es el objetivo de este articulo. Así que, hasta el próximo…

Posdata

Alguna cosilla conectada con esto, aprovecho para escribirla por aqui:

  • La JMM  (java memory model) garantiza que cualquier atributo final de un objeto será totalmente inicializado antes de que publicación del objeto sea visible.
    class Foo {
     private volatile Helper helper;
      public Helper getHelper() {
        return helper;
      }
    
      public void initialize() {
        helper = new Helper(42);
      }
    }
    
    // Immutable Helper
    public final class Helper {
      private final int n;
    
      public Helper(int n) {
        this.n = n;
      }
      // ...
    }

en este ejemplo, helper no va a estar nunca a medio inicializar porque sus atributos son final. Además, es volatile, con lo que todos los hilos verán  el valor actualizado (cuando lo esté).

  • Es necesario tener en cuenta que la JMM permite hacer visible un objeto antes de haber concluido su inicialización. En la construcción de los objetos, primero se asigna valor por defecto a todos los campos y permite asignar memoria al objeto en el lugar apuntado por la referencia (que ya no es null)  antes de ejecutar el constructor, por lo que es posible ver un objeto que existe, con valores por defecto antes de que haya terminado de ejecutarse su constructor.

Para publicar un objeto de forma segura, tanto la referencia al objeto como el estado del objeto deben hacerse visibles a la vez al resto de los hilos.

  • Los bloques sincronizados proveen una garantía mas solida que volatile. Las escrituras previa a la escritura de una variable volatile no pueden ser reordenadas a después del volatile pero las lecturas si. Igualmente, las lecturas despues de volatile no pueden ser movidas a antes del volatile pero las escrituras si.
  • Ahora sí:

FIN

Un pensamiento en “Volatile, o como evitar usar bloqueos en concurrencia

  1. Hugo Villanueva

    Muy buen post, de verdad uno de los mejores en español, me ayudo a comprender como funciona volatile, muchas gracias

    Responder

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *