Esta es una transcripción del episodio hasta que haya notas de episodio disponibles. Podría contener faltas de ortografía o ser difícil de seguir si se está describiendo algo visual.
Una palabra clave muy importante que vas a tener que utilizar en el lenguaje de programación Java cuando haya datos que sean accesibles desde múltiples hilos es la palabra clave volatile.
Esta palabra clave sirve para asegurarse que ciertas optimizaciones que se pueden aplicar a nivel CPU o a nivel compilación que pueden afectar el resultado de un programa cuando es multi-hilo, pues no se ejecuten para así prevenir posibles sistemas de corrupción de memoria.
Este va a ser un episodio un poquito denso porque no voy a poder ejecutar código. Voy a escribirlo pero no vamos a poder ejecutarlo porque esto entra dentro de esa categoría de código que puede que se ejecute si justo en ese momento cuando lo lanzas pues tiras dos dados de 20 y resulta que da 0,0. Entonces no lo voy a ejecutar porque ni me voy a esforzar. Os lo voy a intentar contar lo mejor posible para que lo podáis entender.
La razón por la que existe volatile es porque como digo el procesador a menudo hace ciertas optimizaciones que sobre todo cuando trabajamos con múltiples hilos pueden ser problemáticas. Dos de ellas.
Una es el caso de las cachés. ¿Qué es una caché? Bueno, cuando nosotros estudiamos la arquitectura de un ordenador vemos que tenemos la CPU y tenemos la memoria. En general la CPU cuando recibe una instrucción que es tráete una posición de memoria para calcularla, por ejemplo haz que x sea igual a whatever.whatever, pues lo que suele hacer es que tiene que ir a memoria para sacar la dirección de memoria y las referencias a lo que sea que necesitamos y así hacer su cálculo. O sea, hay razones por las cuales puedes entrar en memoria. Luego la x también la tienes que dejar en memoria. El caso es que realmente en esta caja falta una cosa muy importante que es la caché. Es una memoria muy pequeña que existe junto a la unidad de proceso prácticamente dentro de la CPU, para no tener que salir precisamente a memoria. Y el problema de esta caché es que precisamente su objetivo es hacer que el programa vaya más rápido, pero a veces puede tener ciertas consecuencias. La cosa es que aunque estamos hablando del orden de nanosegundos posiblemente, este acceso a memoria es caro, ¿vale? Porque la memoria RAM, por muy rápida que sea hoy en día, pues sigue siendo más lenta que lo que ocurre dentro de la propia CPU. Entonces cuando tú a una CPU le pides que acceda a memoria para traerse datos, no se va a traer la variable que le has pedido, sino que lo que va a hacer es aplicar ciertas optimizaciones y traerse directamente todo un bloque de memoria, algunos bytes extra, y los va a colocar en la caché. Esta caché, como está dentro de la CPU, los accesos son mucho, mucho, mucho más rápidos. Entonces una instrucción que trate de calcular algo a partir de una variable va a ser mucho más rápida si el contenido de esa variable está guardada en la caché que si está guardada en la memoria real y tiene que ir a por ella. No es de extrañar por lo tanto que en un programa tradicional, cuando tenemos múltiples accesos a variable, pues la caché nos ayude precisamente a que nuestro programa se ejecute ligeramente más rápido.
¿Cuál es el problema? Que esto está muy bien cuando tenemos un único hilo, pero evidentemente si tenemos más de un hilo puede ser problemático. Por ejemplo, imaginad que estamos ejecutando dos hilos que acceden a una variable común o a un objeto común que tiene un dato. Vamos a decir, x es igual a lo que sea. Y justo coincide que en este momento tenemos el hilo 0 y el hilo 1 siendo ejecutados respectivamente por la CPU 0 y por la CPU 1. Es decir, cada CPU está ejecutando un hilo diferente. Entonces ahora imaginemos que justo da la casualidad de que prácticamente en el mismo instante de tiempo ambos hilos tratan de acceder a memoria para actualizar el valor de esta variable x. ¿Qué es lo que puede ocurrir? Pues se puede dar la circunstancia de que la CPU 0 al acceder a memoria para ir a por la x, pues se traiga la x y se traiga algo más y actualice lo que hay en la caché aquí y por lo tanto cuando trabaje con ella trabaje con lo que hay en la caché. Y puede ocurrir que la CPU 1 haga lo mismo, que se traiga su x aquí y la deje aquí en la caché para trabajar con ella. Y en este momento la x va a tener el valor que tenía la última vez que accedió a memoria. Por lo tanto, si x en memoria vale 1, pues x en la caché de 0 va a valer 1 y x en la caché de 1 también va a valer 1.
Ahora ¿qué pasa? Imaginemos que el hilo de la derecha hace una actualización de lo que vale esta variable y la cambia de valor y por lo tanto ahora x vale 2, no vale 1. ¿Qué pasa si en un instante de tiempo muy corto la CPU 0 trate de acceder otra vez a consultar qué es lo que vale en este momento la variable x? Bueno, pues puede ocurrir que la CPU 0 diga, ah, como la x está precisamente en la caché, no necesito ir a la memoria a consultar lo que vale porque la tengo aquí. Claro, el problema es que lo que vale la caché y lo que vale la memoria es diferente porque otro hilo ha modificado el valor de esa variable sin que te puedas enterar. Entonces es posible que si haces cálculos utilizando el valor de la caché corrompas la memoria. Y esto es muy sutil y como te has que depurar esto te vas a perder la cabeza.
Así que para evitar estas cosas usamos la palabra clave volatile. Cuando declaramos un atributo que puede ser accesible desde múltiples hilos y que es susceptible por lo tanto de que esto ocurra, lo suyo es que no solamente lo declaremos como por ejemplo y en la tx igual a 0, sino que pongamos la palabra clave volatile. Cuando aplicamos la palabra clave volatile lo que hacemos es introducir una barrera en el código, que se asegura de que en este caso concreto no ocurra nada de lo que os he contado. Si un atributo está declarado como volatile lo que va a ocurrir es que cualquier optimización de caché se va a desactivar. Por lo tanto, cuando se haga un acceso a lo que vale una variable que está marcada como volatile, el código se asegurará de que se invalide siempre la caché y siempre se haga un acceso fresco a memoria. De este modo, si justo en ese mismo microsegundo otro hilo ha modificado lo que vale esa posición de memoria, la lectura que se haga va a ser fresca porque va a ser el valor que hay memoria y no el que pueda haber en la caché. Por supuesto, aquí estamos dando por sentado que tenemos otros mecanismos de concurrencia que garanticen un acceso ordenado a esta variable, como los que hemos estudiado anteriormente. Pero aquí lo importante es que el acceso a esa variable se hace de una forma ordenada.
Otra razón por la cual la palabra clave volatile nos puede resultar de utilidad es para impedir la ordenación de instrucciones. Esta es otra situación que a veces ocurre en los procesadores modernos, aunque realmente no tan modernos. A veces una CPU puede determinar que si tenía que hacer varias instrucciones, como por ejemplo x es igual a acceso 0 y es igual a acceso 1 y luego tiene que hacer un cálculo que involucre por ejemplo una variable pero no la otra, pues la CPU puede determinar que si esta instrucción de aquí va a tardar más que esta, pues como en principio la segunda no depende de la primera, pues por ciertas optimizaciones de compilación y ejecución se puede invertir el orden, de tal manera que así el resultado del cálculo está disponible antes y luego la CPU puede invertir más tiempo en hacer el acceso a memoria para recuperar el otro valor.
Claro, ¿qué ocurre? Que de nuevo puede ocurrir que a lo mejor una variable sea importante que el valor con el que se trabaja sea fresco y puede que por hacer este tipo de optimizaciones, sobre todo si el programa es multihilo, se corrompa precisamente el valor porque luego lo que otro hilo va a consultar puede que llegue en el momento inapropiado y puede que le da un valor incorrecto. La palabra clave volatile actúa también de barrera de compilación, es decir, esta optimización nunca se va a hacer y las instrucciones se van a hacer siempre tal cual en el orden que las hemos escrito sin que se altere de ningún modo la secuencia de instrucciones que ejecuta la CPU cuando traducimos este código a sentencias de CPU.
Por lo tanto, con la palabra clave volatile actuamos, como digo, como una barrera que impide que esto ocurra. Aunque yo lo haya puesto con enteros, una última cosa que tienes que tener en cuenta es que volatile te vale para cualquier atributo, atributo, y me da igual si es primitivo, si es un objeto, ahora bien, tiene que ser un atributo, porque es también lo que más fácil puedes compartir.
Y eso sería todo en esta lección, te dejo el enlace a la lista de reproducción de concurrencia por si quieres echar un vistazo a más vídeos de esta lista y nosotros nos vemos en próximas lecciones. Un saludo y hasta luego.