JavaRush /Blog Java /Random-ES /Gestión de flujo. La palabra clave volátil y el método yi...

Gestión de flujo. La palabra clave volátil y el método yield()

Publicado en el grupo Random-ES
¡Hola! Seguimos estudiando el subproceso múltiple y hoy nos familiarizaremos con una nueva palabra clave: volátil y el método yield(). Averigüemos qué es :)

Palabra clave volátil

A la hora de crear aplicaciones multiproceso podemos enfrentarnos a dos problemas graves. En primer lugar, durante el funcionamiento de una aplicación multiproceso, diferentes subprocesos pueden almacenar en caché los valores de las variables (hablaremos más sobre esto en la conferencia "Uso de volátiles" ). Es posible que un hilo haya cambiado el valor de una variable, pero el segundo no haya visto este cambio porque estaba trabajando con su propia copia almacenada en caché de la variable. Naturalmente, las consecuencias pueden ser graves. Imagínese que esto no es solo una especie de "variable", sino, por ejemplo, el saldo de su tarjeta bancaria, que de repente comenzó a saltar aleatoriamente de un lado a otro :) No es muy agradable, ¿verdad? En segundo lugar, en Java, las operaciones de lectura y escritura en campos de todo tipo excepto longy doubleson atómicas. ¿Qué es la atomicidad? Bueno, por ejemplo, si cambias el valor de una variable en un hilo inty en otro hilo lees el valor de esta variable, obtendrás su valor anterior o uno nuevo, el que resultó después del cambio en Hilo 1. No aparecerán “opciones intermedias” allí. Tal vez. Sin embargo, esto no funciona con longy . double¿Por qué? Porque es multiplataforma. ¿Recuerdas que dijimos en los primeros niveles que el principio de Java está "escrito una vez, funciona en todas partes"? Esto es multiplataforma. Es decir, una aplicación Java se ejecuta en plataformas completamente diferentes. Por ejemplo, en los sistemas operativos Windows, en diferentes versiones de Linux o MacOS y en todas partes, esta aplicación funcionará de manera estable. longy double- las primitivas más "pesadas" de Java: pesan 64 bits. Y algunas plataformas de 32 bits simplemente no implementan la atomicidad de lectura y escritura de variables de 64 bits. Estas variables se leen y escriben en dos operaciones. Primero, se escriben los primeros 32 bits en la variable, luego otros 32. En consecuencia, en estos casos puede surgir un problema. Un hilo escribe algún valor de 64 bits en una variableХ, y lo hace “en dos pasos”. Al mismo tiempo, el segundo hilo intenta leer el valor de esta variable, y lo hace justo en el medio, cuando ya se han escrito los primeros 32 bits, pero los segundos aún no se han escrito. Como resultado, lee un valor intermedio incorrecto y se produce un error. Por ejemplo, si en una plataforma de este tipo intentamos escribir un número en una variable (9223372036854775809), ocupará 64 bits. En formato binario se verá así: 1000000000000000000000000000000000000000000000000000000000000001 El primer hilo comenzará a escribir este número en una variable y primero escribirá los primeros 32 bits: 10000000000000000000000000000000 0000 00000 y luego el segundo 32: 00000000000000000000000000000001 Y un segundo hilo puede encajar en este espacio y lea el valor intermedio de la variable - 1000000000000000000000000000000000, los primeros 32 bits que ya se han escrito. En el sistema decimal, este número es igual a 2147483648. Es decir, solo queríamos escribir el número 9223372036854775809 en una variable, pero debido a que esta operación en algunas plataformas no es atómica, obtuvimos el número "izquierdo" 2147483648 , que no necesitamos, surgió de la nada y se desconoce cómo afectará al funcionamiento del programa. El segundo hilo simplemente leyó el valor de la variable antes de que finalmente se escribiera, es decir, vio los primeros 32 bits, pero no los segundos 32 bits. Estos problemas, por supuesto, no surgieron ayer y en Java se resuelven utilizando una sola palabra clave: volátil . Si declaramos alguna variable en nuestro programa con la palabra volátil...
public class Main {

   public volatile long x = 2222222222222222222L;

   public static void main(String[] args) {

   }
}
…esto significa que:
  1. Siempre será leído y escrito atómicamente. Incluso si es de 64 bits doubleo long.
  2. La máquina Java no lo almacenará en caché. Por lo tanto, se excluye la situación en la que 10 subprocesos funcionan con sus copias locales.
Así se solucionan dos problemas muy graves en una palabra :)

método rendimiento()

Ya hemos visto muchos métodos de la clase Thread, pero hay uno importante que será nuevo para usted. Este es el método rendimiento() . Traducido del inglés como "ceder". ¡Y eso es exactamente lo que hace el método! Gestión de flujo.  La palabra clave volátil y el método yield() - 2Cuando llamamos al método de rendimiento en un subproceso, en realidad les dice a otros subprocesos: "Está bien, muchachos, no tengo mucha prisa, así que si es importante para alguno de ustedes obtener tiempo de CPU, tómenlo, lo haré". no urgente." A continuación se muestra un ejemplo sencillo de cómo funciona:
public class ThreadExample extends Thread {

   public ThreadExample() {
       this.start();
   }

   public void run() {

       System.out.println(Thread.currentThread().getName() + "dar paso a los demás");
       Thread.yield();
       System.out.println(Thread.currentThread().getName() + " has finished executing.");
   }

   public static void main(String[] args) {
       new ThreadExample();
       new ThreadExample();
       new ThreadExample();
   }
}
Creamos y lanzamos secuencialmente tres subprocesos: Thread-0, Thread-1y Thread-2. Thread-0comienza primero e inmediatamente da paso a otros. Después comienza Thread-1y también cede. Después de eso, comienza Thread-2, que también es inferior. No tenemos más hilos, y después de que Thread-2el último haya cedido su lugar, el programador de hilos mira: “Entonces, no hay más hilos nuevos, ¿a quién tenemos en la cola? ¿Quién fue el último en ceder su lugar antes Thread-2? Yo pienso que fue Thread-1? Está bien, que así sea”. Thread-1hace su trabajo hasta el final, después de lo cual el programador de subprocesos continúa coordinando: “Está bien, el subproceso 1 se ha completado. ¿Tenemos a alguien más en la fila?" Hay Thread-0 en la cola: cedió su lugar inmediatamente antes del Thread-1. Ahora el asunto le ha llegado y lo están llevando a cabo hasta el final. Luego de lo cual el programador termina de coordinar los subprocesos: “Está bien, Subproceso-2, le diste paso a otros subprocesos, todos ya funcionaron. Fuiste el último en ceder, así que ahora te toca a ti”. Después de esto, Thread-2 se ejecuta hasta su finalización. La salida de la consola se verá así: Thread-0 da paso a otros Thread-1 da paso a otros Thread-2 da paso a otros Thread-1 ha terminado de ejecutarse. Thread-0 ha terminado de ejecutarse. Thread-2 ha terminado de ejecutarse. El programador de subprocesos, por supuesto, puede ejecutar subprocesos en un orden diferente (por ejemplo, 2-1-0 en lugar de 0-1-2), pero el principio es el mismo.

Sucede antes de las reglas

Lo último que tocaremos hoy son los principios de " sucede antes ". Como ya sabe, en Java, la mayor parte del trabajo de asignar tiempo y recursos a los subprocesos para completar sus tareas lo realiza el programador de subprocesos. Además, ha visto más de una vez cómo los subprocesos se ejecutan en un orden arbitrario y, en la mayoría de los casos, es imposible predecirlo. Y, en general, después de la programación "secuencial" que hicimos antes, el subproceso múltiple parece algo aleatorio. Como ya ha visto, el progreso de un programa multiproceso se puede controlar mediante un conjunto completo de métodos. Pero además de esto, en el subproceso múltiple de Java hay otra "isla de estabilidad": 4 reglas llamadas " sucede antes ". Literalmente del inglés, esto se traduce como "sucede antes" o "sucede antes". El significado de estas reglas es bastante sencillo de entender. Imaginemos que tenemos dos hilos: Ay B. Cada uno de estos subprocesos puede realizar operaciones 1y 2. Y cuando en cada una de las reglas decimos “ A sucede-antes de B ”, esto significa que todos los cambios realizados por el hilo Aantes de la operación 1y los cambios que esta operación conllevó son visibles para el hilo Ben el momento en que se realiza la operación 2y después de realizada la operación. Cada una de estas reglas garantiza que al escribir un programa multiproceso, algunos eventos sucederán antes que otros el 100% del tiempo, y que el hilo Ben el momento de la operación 2siempre estará al tanto de los cambios que Аrealizó durante la operación. 1. Mirémoslos.

Regla 1.

La liberación de un mutex ocurre antes de que otro subproceso adquiera el mismo monitor. Bueno, aquí todo parece claro. Si el mutex de un objeto o clase es adquirido por un hilo, por ejemplo, un hilo А, otro hilo (hilo B) no puede adquirirlo al mismo tiempo. Debe esperar hasta que se libere el mutex.

Regla 2.

El método Thread.start() sucede antes Thread.run() . Nada complicado tampoco. Ya lo sabes: para que el código dentro del método comience a ejecutarse run(), debes llamar al método en el hilo start(). ¡Es suyo y no del método en sí run()! Esta regla garantiza que Thread.start()los valores de todas las variables establecidas antes de la ejecución serán visibles dentro del método que comenzó a ejecutarse run().

Regla 3.

La finalización del método run() ocurre antes de la salida del método join(). Volvamos a nuestras dos corrientes: Аy B. Llamamos al método join()de tal manera que el hilo Bdebe esperar hasta que se complete Aantes de realizar su trabajo. Esto significa que el método run()del objeto A definitivamente se ejecutará hasta el final. Y todos los cambios en los datos que ocurren en el método run()del subproceso Aserán completamente visibles en el subproceso Bcuando espere a que finalice Ay comience a funcionar por sí solo.

Regla 4.

La escritura en una variable volátil ocurre antes de leer de la misma variable. Al utilizar la palabra clave volátil, de hecho, siempre obtendremos el valor actual. Incluso en el caso de longy double, cuyos problemas se discutieron anteriormente. Como ya comprenderá, los cambios realizados en algunos hilos no siempre son visibles para otros hilos. Pero, por supuesto, muy a menudo hay situaciones en las que ese comportamiento del programa no nos conviene. Digamos que asignamos un valor a una variable en un hilo A:
int z;.

z= 555;
Si nuestro hilo Bimprimiera el valor de una variable zen la consola, fácilmente podría imprimir 0 porque no conoce el valor asignado a ella. Entonces, la Regla 4 nos garantiza: si declara una variable zcomo volátil, los cambios en sus valores en un hilo siempre serán visibles en otro hilo. Si al código anterior le sumamos la palabra volátil...
volatile int z;.

z= 555;
...se excluye la situación en la que la transmisión Bgenerará 0 en la consola. La escritura en variables volátiles ocurre antes de leer de ellas.
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION