¡Hola! Continuamos nuestro estudio de subprocesos múltiples. Hoy conoceremos la
Cuando llamamos al
volatile
palabra clave y el yield()
método. Vamos a sumergirnos :)
La palabra clave volátil
A la hora de crear aplicaciones multiproceso, podemos toparnos con dos serios problemas. En primer lugar, cuando se ejecuta una aplicación de subprocesos múltiples, diferentes subprocesos pueden almacenar en caché los valores de las variables (ya hablamos de esto en la lección titulada 'Uso de volátiles' ). Puede tener la situación en la que un subproceso cambia el valor de una variable, pero un segundo subproceso no ve el cambio, porque está trabajando con su copia en caché de la variable. Naturalmente, las consecuencias pueden ser graves. Supongamos que no se trata de una variable cualquiera, sino del saldo de su cuenta bancaria, que de repente comienza a saltar al azar hacia arriba y hacia abajo :) Eso no suena divertido, ¿verdad? En segundo lugar, en Java, las operaciones para leer y escribir todos los tipos primitivos,long
double
, son atómicos. Bueno, por ejemplo, si cambia el valor de una int
variable en un subproceso y en otro subproceso lee el valor de la variable, obtendrá su valor anterior o el nuevo, es decir, el valor que resultó del cambio. en el hilo 1. No hay 'valores intermedios'. Sin embargo, esto no funciona con long
s y double
s. ¿Por qué? Debido al soporte multiplataforma. ¿Recuerdas en los niveles iniciales que dijimos que el principio rector de Java es "escribir una vez, ejecutar en cualquier lugar"? Eso significa soporte multiplataforma. En otras palabras, una aplicación Java se ejecuta en todo tipo de plataformas diferentes. Por ejemplo, en sistemas operativos Windows, diferentes versiones de Linux o MacOS. Se ejecutará sin problemas en todos ellos. Pesando en un 64 bits,long
double
son las primitivas 'más pesadas' en Java. Y ciertas plataformas de 32 bits simplemente no implementan la lectura y escritura atómica de variables de 64 bits. Estas variables se leen y escriben en dos operaciones. Primero, los primeros 32 bits se escriben en la variable y luego se escriben otros 32 bits. Como resultado, puede surgir un problema. Un subproceso escribe un valor de 64 bits en una X
variable y lo hace en dos operaciones. Al mismo tiempo, un segundo hilo intenta leer el valor de la variable y lo hace entre esas dos operaciones, cuando se han escrito los primeros 32 bits, pero no los segundos 32 bits. Como resultado, lee un valor intermedio incorrecto y tenemos un error. Por ejemplo, si en una plataforma de este tipo intentamos escribir el número a un 9223372036854775809 a una variable, ocupará 64 bits. En forma binaria, se ve así: 10000000000000000000000000000000000000000000000000000000000000001 El primer hilo comienza a escribir el número en la variable. Primero escribe los primeros 32 bits (10000000000000000000000000000000) y luego los segundos 32 bits (00000000000000000000000000000001) Y el segundo hilo puede quedar atrapado entre estas operaciones, leyendo el valor intermedio de la variable (100000000000000000000000000000000), que son los primeros 32 bits que ya se han escrito. En el sistema decimal, este número es 2.147.483.648. En otras palabras, solo queríamos escribir el número 9223372036854775809 en una variable, pero debido a que esta operación no es atómica en algunas plataformas, tenemos el número maligno 2,147,483,648, que salió de la nada y tendrá un efecto desconocido el programa. El segundo hilo simplemente leyó el valor de la variable antes de que terminara de escribirse, es decir, el hilo vio los primeros 32 bits, pero no los segundos 32 bits. Por supuesto, estos problemas no surgieron ayer. Java los resuelve con una sola palabra clave: volatile
. Si usamos elvolatile
palabra clave al declarar alguna variable en nuestro programa…
public class Main {
public volatile long x = 2222222222222222222L;
public static void main(String[] args) {
}
}
…esto significa que:
- Siempre se leerá y escribirá atómicamente. Incluso si es de 64 bits
double
olong
. - La máquina Java no lo almacenará en caché. Por lo tanto, no tendrá una situación en la que 10 subprocesos estén trabajando con sus propias copias locales.
El método de rendimiento ()
Ya hemos revisado muchos de losThread
métodos de la clase, pero hay uno importante que será nuevo para usted. Es el yield()
método . ¡Y hace exactamente lo que su nombre implica! 
yield
método en un subproceso, en realidad se comunica con los otros subprocesos: 'Hola, muchachos. No tengo ninguna prisa particular por ir a ninguna parte, así que si es importante para alguno de ustedes obtener tiempo de procesamiento, tómelo, puedo esperar”. He aquí un ejemplo simple de cómo funciona esto:
public class ThreadExample extends Thread {
public ThreadExample() {
this.start();
}
public void run() {
System.out.println(Thread.currentThread().getName() + " yields its place to others");
Thread.yield();
System.out.println(Thread.currentThread().getName() + " has finished executing.");
}
public static void main(String[] args) {
new ThreadExample();
new ThreadExample();
new ThreadExample();
}
}
Creamos e iniciamos secuencialmente tres subprocesos: Thread-0
, Thread-1
y Thread-2
. Thread-0
comienza primero e inmediatamente cede a los demás. Luego Thread-1
se arranca y también cede. Entonces Thread-2
se arranca, que también cede. No tenemos más subprocesos, y después Thread-2
de ceder su último lugar, el programador de subprocesos dice: 'Hmm, no hay más subprocesos nuevos. ¿A quién tenemos en la cola? ¿ Quién cedió su lugar antes Thread-2
? Parece que lo fue Thread-1
. Bien, eso significa que lo dejaremos correr'. Thread-1
completa su trabajo y luego el planificador de subprocesos continúa su coordinación: 'Está bien, Thread-1
terminado. ¿Tenemos a alguien más en la cola?'. Thread-0 está en la cola: cedió su lugar justo antesThread-1
. Ahora llega su turno y corre hasta su finalización. Luego, el planificador termina de coordinar los subprocesos: 'Está bien, Thread-2
cedió el paso a otros subprocesos y ya están todos terminados. Fuiste el último en ceder, así que ahora es tu turno'. Luego Thread-2
corre hasta completarse. La salida de la consola se verá así: Thread-0 cede su lugar a otros Thread-1 cede su lugar a otros Thread-2 cede su lugar a otros Thread-1 ha terminado de ejecutarse. Thread-0 ha terminado de ejecutarse. Thread-2 ha terminado de ejecutarse. Por supuesto, el planificador de subprocesos puede iniciar los subprocesos en un orden diferente (por ejemplo, 2-1-0 en lugar de 0-1-2), pero el principio sigue siendo el mismo.
Sucede antes de las reglas
Lo último que tocaremos hoy es el concepto de ' sucede antes '. Como ya sabe, en Java, el programador de subprocesos realiza la mayor parte del trabajo relacionado con la asignación de tiempo y recursos a los subprocesos para realizar sus tareas. También ha visto repetidamente cómo se ejecutan los subprocesos en un orden aleatorio que, por lo general, es imposible de predecir. Y en general, después de la programación 'secuencial' que hicimos anteriormente, la programación multiproceso parece algo aleatorio. Ya ha llegado a creer que puede usar una gran cantidad de métodos para controlar el flujo de un programa de subprocesos múltiples. Pero los subprocesos múltiples en Java tienen un pilar más: las 4 reglas ' sucede antes '. Comprender estas reglas es bastante simple. Imagine que tenemos dos hilos,A
yB
. Cada uno de estos subprocesos puede realizar operaciones 1
y 2
. En cada regla, cuando decimos ' A sucede antes que B ', queremos decir que todos los cambios realizados por el subproceso A
antes de la operación 1
y los cambios resultantes de esta operación son visibles para el subproceso B
cuando 2
se realiza la operación y posteriormente. Cada regla garantiza que cuando escribe un programa multiproceso, ciertos eventos ocurrirán antes que otros el 100% del tiempo, y que en el momento de la operación, el 2
subproceso B
siempre estará al tanto de los cambios que ese subproceso A
realizó durante la operación 1
. Vamos a repasarlos.
Regla 1.
La liberación de un mutex ocurre antes de que otro subproceso adquiera el mismo monitor. Creo que entiendes todo aquí. Si el mutex de un objeto o clase es adquirido por un subproceso, por ejemplo, por subprocesoA
, otro subproceso (subproceso B
) no puede adquirirlo al mismo tiempo. Debe esperar hasta que se libere el mutex.
regla 2
ElThread.start()
método ocurre antes Thread.run()
. Una vez más, nada difícil aquí. Ya sabe que para comenzar a ejecutar el código dentro del run()
método, debe llamar al start()
método en el hilo. Específicamente, el método de inicio, ¡no el run()
método en sí! Esta regla asegura que los valores de todas las variables establecidas antes Thread.start()
de llamar serán visibles dentro del run()
método una vez que comience.
Regla 3.
El final delrun()
método ocurre antes del regreso del join()
método. Volvamos a nuestros dos hilos: A
y B
. Llamamos al join()
método para garantizar que el subproceso B
espere a que se complete A
antes de que haga su trabajo. Esto significa que se garantiza que el método del objeto A run()
se ejecutará hasta el final. Y todos los cambios en los datos que ocurren en el run()
método de subproceso A
están cien por ciento garantizados para ser visibles en el subproceso B
una vez que finaliza esperando que el subproceso A
termine su trabajo para que pueda comenzar su propio trabajo.
Regla 4.
Escribir en unavolatile
variable ocurre antes de leer de esa misma variable. Cuando usamos la volatile
palabra clave, en realidad siempre obtenemos el valor actual. Incluso con una long
o double
(hablamos antes sobre los problemas que pueden ocurrir aquí). Como ya comprenderá, los cambios realizados en algunos subprocesos no siempre son visibles para otros subprocesos. Pero, por supuesto, hay situaciones muy frecuentes en las que ese comportamiento no nos conviene. Supongamos que asignamos un valor a una variable en hilo A
:
int z;
….
z = 555;
Si nuestro B
subproceso debe mostrar el valor de la z
variable en la consola, fácilmente podría mostrar 0, porque no conoce el valor asignado. Pero la Regla 4 garantiza que si declaramos la z
variable como volatile
, los cambios en su valor en un subproceso siempre serán visibles en otro subproceso. Si le sumamos la palabra volatile
al código anterior...
volatile int z;
….
z = 555;
... luego prevenimos la situación en la que el subproceso B
podría mostrar 0. La escritura en las volatile
variables ocurre antes de leerlas.
GO TO FULL VERSION