CodeGym /Blog Java /Random-ES /Mejor juntos: Java y la clase Thread. Parte III — Interac...
John Squirrels
Nivel 41
San Francisco

Mejor juntos: Java y la clase Thread. Parte III — Interacción

Publicado en el grupo Random-ES
Una breve descripción de los detalles de cómo interactúan los subprocesos. Anteriormente, vimos cómo se sincronizan los subprocesos entre sí. Esta vez nos sumergiremos en los problemas que pueden surgir cuando los subprocesos interactúan y hablaremos sobre cómo evitarlos. También proporcionaremos algunos enlaces útiles para un estudio más profundo. Mejor juntos: Java y la clase Thread.  Parte III — Interacción - 1

Introducción

Entonces, sabemos que Java tiene hilos. Puede leer sobre eso en la revisión titulada Mejor juntos: Java y la clase Thread. Parte I — Hilos de ejecución . Y exploramos el hecho de que los subprocesos pueden sincronizarse entre sí en la revisión titulada Mejor juntos: Java y la clase Thread. Parte II — Sincronización . Es hora de hablar sobre cómo los hilos interactúan entre sí. ¿Cómo comparten los recursos compartidos? ¿Qué problemas pueden surgir aquí? Mejor juntos: Java y la clase Thread.  Parte III — Interacción - 2

Punto muerto

El problema más aterrador de todos es el punto muerto. Interbloqueo es cuando dos o más subprocesos están eternamente esperando el otro. Tomaremos un ejemplo de la página web de Oracle que describe el interbloqueo :

public class Deadlock {
    static class Friend {
        private final String name;
        public Friend(String name) {
            this.name = name;
        }
        public String getName() {
            return this.name;
        }
        public synchronized void bow(Friend bower) {
            System.out.format("%s: %s bowed to me!%n",
                    this.name, bower.getName());
            bower.bowBack(this);
        }
        public synchronized void bowBack(Friend bower) {
            System.out.format("%s: %s bowed back to me!%n",
                    this.name, bower.getName());
        }
    }

    public static void main(String[] args) {
        final Friend alphonse = new Friend("Alphonse");
        final Friend gaston = new Friend("Gaston");
        new Thread(() -> alphonse.bow(gaston)).start();
        new Thread(() -> gaston.bow(alphonse)).start();
    }
}
Es posible que no se produzca un interbloqueo aquí la primera vez, pero si su programa se cuelga, entonces es hora de ejecutarlo jvisualvm: Mejor juntos: Java y la clase Thread.  Parte III — Interacción - 3con un complemento JVisualVM instalado (a través de Herramientas -> Complementos), podemos ver dónde ocurrió el interbloqueo:

"Thread-1" - Thread t@12
   java.lang.Thread.State: BLOCKED
	at Deadlock$Friend.bowBack(Deadlock.java:16)
	- waiting to lock <33a78231> (a Deadlock$Friend) owned by "Thread-0" t@11
El subproceso 1 está esperando el bloqueo del subproceso 0. ¿Por qué sucede eso? Thread-1comienza a ejecutarse y ejecuta el Friend#bowmétodo. Está marcado con la synchronizedpalabra clave, lo que significa que estamos adquiriendo el monitor para this(el objeto actual). La entrada del método era una referencia al otro Friendobjeto. Ahora, Thread-1quiere ejecutar el método en el otro Friendy debe adquirir su bloqueo para hacerlo. Pero si el otro subproceso (en este caso Thread-0) logró ingresar al bow()método, entonces el bloqueo ya se adquirió y Thread-1esperaThread-0, y viceversa. Este callejón sin salida es irresoluble, y lo llamamos punto muerto. Como un agarre mortal que no se puede soltar, el interbloqueo es un bloqueo mutuo que no se puede romper. Para otra explicación de interbloqueo, puede ver este video: Explicación de interbloqueo y Livelock .

Livelock

Si hay interbloqueo, ¿también hay livelock? Sí, hay :) Livelock ocurre cuando los subprocesos parecen estar vivos, pero no pueden hacer nada, porque no se pueden cumplir las condiciones requeridas para que continúen su trabajo. Básicamente, livelock es similar a deadlock, pero los subprocesos no se "cuelan" esperando un monitor. En cambio, siempre están haciendo algo. Por ejemplo:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class App {
    public static final String ANSI_BLUE = "\u001B[34m";
    public static final String ANSI_PURPLE = "\u001B[35m";
    
    public static void log(String text) {
        String name = Thread.currentThread().getName(); // Like "Thread-1" or "Thread-0"
        String color = ANSI_BLUE;
        int val = Integer.valueOf(name.substring(name.lastIndexOf("-") + 1)) + 1;
        if (val != 0) {
            color = ANSI_PURPLE;
        }
        System.out.println(color + name + ": " + text + color);
        try {
            System.out.println(color + name + ": wait for " + val + " sec" + color);
            Thread.currentThread().sleep(val * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Lock first = new ReentrantLock();
        Lock second = new ReentrantLock();

        Runnable locker = () -> {
            boolean firstLocked = false;
            boolean secondLocked = false;
            try {
                while (!firstLocked || !secondLocked) {
                    firstLocked = first.tryLock(100, TimeUnit.MILLISECONDS);
                    log("First Locked: " + firstLocked);
                    secondLocked = second.tryLock(100, TimeUnit.MILLISECONDS);
                    log("Second Locked: " + secondLocked);
                }
                first.unlock();
                second.unlock();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };

        new Thread(locker).start();
        new Thread(locker).start();
    }
}
El éxito de este código depende del orden en que el planificador de subprocesos de Java inicia los subprocesos. Si Thead-1comienza primero, obtenemos livelock:

Thread-1: First Locked: true
Thread-1: wait for 2 sec
Thread-0: First Locked: false
Thread-0: wait for 1 sec
Thread-0: Second Locked: true
Thread-0: wait for 1 sec
Thread-1: Second Locked: false
Thread-1: wait for 2 sec
Thread-0: First Locked: false
Thread-0: wait for 1 sec
...
Como puede ver en el ejemplo, ambos subprocesos intentan adquirir ambos bloqueos a la vez, pero fallan. Pero, no están en punto muerto. Exteriormente, todo está bien y están haciendo su trabajo. Mejor juntos: Java y la clase Thread.  Parte III — Interacción - 4De acuerdo con JVisualVM, vemos períodos de suspensión y un período de estacionamiento (esto es cuando un subproceso intenta adquirir un bloqueo: ingresa al estado de estacionamiento, como discutimos anteriormente cuando hablamos sobre la sincronización de subprocesos ) . Puede ver un ejemplo de livelock aquí: Java - Thread Livelock .

Inanición

Además del interbloqueo y el bloqueo dinámico, existe otro problema que puede ocurrir durante el subprocesamiento múltiple: la inanición. Este fenómeno difiere de las formas anteriores de bloqueo en que los subprocesos no están bloqueados, simplemente no tienen suficientes recursos. Como resultado, mientras que algunos subprocesos toman todo el tiempo de ejecución, otros no pueden ejecutarse: Mejor juntos: Java y la clase Thread.  Parte III — Interacción - 5

https://www.logicbig.com/

Puedes ver un súper ejemplo aquí: Java - Thread Starvation and Fairness . Este ejemplo muestra lo que sucede con los subprocesos durante la inanición y cómo un pequeño cambio de Thread.sleep()a Thread.wait()le permite distribuir la carga de manera uniforme. Mejor juntos: Java y la clase Thread.  Parte III — Interacción - 6

Condiciones de carrera

En subprocesos múltiples, existe una "condición de carrera". Este fenómeno ocurre cuando los subprocesos comparten un recurso, pero el código está escrito de una manera que no garantiza un intercambio correcto. Echa un vistazo a un ejemplo:

public class App {
    public static int value = 0;

    public static void main(String[] args) {
        Runnable task = () -> {
            for (int i = 0; i < 10000; i++) {
                int oldValue = value;
                int newValue = ++value;
                if (oldValue + 1 != newValue) {
                    throw new IllegalStateException(oldValue + " + 1 = " + newValue);
                }
            }
        };
        new Thread(task).start();
        new Thread(task).start();
        new Thread(task).start();
    }
}
Es posible que este código no genere un error la primera vez. Cuando lo hace, puede verse así:

Exception in thread "Thread-1" java.lang.IllegalStateException: 7899 + 1 = 7901
	at App.lambda$main$0(App.java:13)
	at java.lang.Thread.run(Thread.java:745)
Como puede ver, algo salió mal mientras newValuese le asignaba un valor. newValuees demasiado grande. Debido a la condición de carrera, uno de los subprocesos logró cambiar las variables valueentre las dos declaraciones. Resulta que hay una carrera entre los hilos. Ahora piense en lo importante que es no cometer errores similares con las transacciones monetarias... También se pueden ver ejemplos y diagramas aquí: Código para simular la condición de carrera en el subproceso de Java .

Volátil

Hablando de la interacción de los hilos, volatilevale la pena mencionar la palabra clave. Veamos un ejemplo sencillo:

public class App {
    public static boolean flag = false;

    public static void main(String[] args) throws InterruptedException {
        Runnable whileFlagFalse = () -> {
            while(!flag) {
            }
            System.out.println("Flag is now TRUE");
        };

        new Thread(whileFlagFalse).start();
        Thread.sleep(1000);
        flag = true;
    }
}
Lo más interesante es que es muy probable que esto no funcione. El nuevo hilo no verá el cambio en el flagcampo. Para arreglar esto para el flagcampo, necesitamos usar la volatilepalabra clave. ¿Como y por qué? El procesador realiza todas las acciones. Pero los resultados de los cálculos deben almacenarse en algún lugar. Para ello, existe la memoria principal y existe la caché del procesador. Los cachés de un procesador son como una pequeña porción de memoria que se usa para acceder a los datos más rápidamente que cuando se accede a la memoria principal. Pero todo tiene un inconveniente: los datos en el caché pueden no estar actualizados (como en el ejemplo anterior, cuando el valor del campo de la bandera no se actualizó). Entonces elvolatileLa palabra clave le dice a la JVM que no queremos almacenar en caché nuestra variable. Esto permite que el resultado actualizado se vea en todos los subprocesos. Esta es una explicación muy simplificada. En cuanto a la volatilepalabra clave, te recomiendo que leas este artículo . Para obtener más información, también le aconsejo que lea Java Memory Model y Java Volatile Keyword . Además, es importante recordar que volatilese trata de la visibilidad y no de la atomicidad de los cambios. Mirando el código en la sección "Condiciones de carrera", veremos una información sobre herramientas en IntelliJ IDEA: Mejor juntos: Java y la clase Thread.  Parte III — Interacción - 7Esta inspección se agregó a IntelliJ IDEA como parte del problema IDEA-61117 , que se incluyó en las Notas de la versión en 2010.

Atomicidad

Las operaciones atómicas son operaciones que no se pueden dividir. Por ejemplo, la operación de asignar un valor a una variable debe ser atómica. Desafortunadamente, la operación de incremento no es atómica, porque el incremento requiere hasta tres operaciones de CPU: obtener el valor anterior, agregarle uno y luego guardar el valor. ¿Por qué es importante la atomicidad? Con la operación de incremento, si hay una condición de carrera, entonces el recurso compartido (es decir, el valor compartido) puede cambiar repentinamente en cualquier momento. Además, las operaciones que involucran estructuras de 64 bits, por ejemplo longy double, no son atómicas. Se pueden leer más detalles aquí: Asegure la atomicidad al leer y escribir valores de 64 bits . Los problemas relacionados con la atomicidad se pueden ver en este ejemplo:

public class App {
    public static int value = 0;
    public static AtomicInteger atomic = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Runnable task = () -> {
            for (int i = 0; i < 10000; i++) {
                value++;
                atomic.incrementAndGet();
            }
        };
        for (int i = 0; i < 3; i++) {
            new Thread(task).start();
        }
        Thread.sleep(300);
        System.out.println(value);
        System.out.println(atomic.get());
    }
}
La clase especial AtomicIntegersiempre nos dará 30.000, pero cambiará valuede vez en cuando. Hay una breve descripción general de este tema: Introducción a las variables atómicas en Java . El algoritmo de "comparar e intercambiar" se encuentra en el corazón de las clases atómicas. Puede leer más sobre esto aquí en Comparación de algoritmos sin bloqueo: CAS y FAA en el ejemplo de JDK 7 y 8 o en el artículo Comparar e intercambiar en Wikipedia.Mejor juntos: Java y la clase Thread.  Parte III — Interacción - 9

http://jeremymanson.blogspot.com/2008/11/what-volatile-means-in-java.html

Sucede-antes

Hay un concepto interesante y misterioso llamado "sucede antes". Como parte de su estudio de los subprocesos, debe leer al respecto. La relación "sucede antes" muestra el orden en que se verán las acciones entre subprocesos. Hay muchas interpretaciones y comentarios. Esta es una de las presentaciones más recientes sobre este tema: Java "Happens-Before" Relationships .

Resumen

En esta revisión, hemos explorado algunos de los detalles de cómo interactúan los hilos. Discutimos los problemas que pueden surgir, así como las formas de identificarlos y eliminarlos. Lista de materiales adicionales sobre el tema: Mejor juntos: Java y la clase Thread. Parte I — Hilos de ejecución Mejor juntos: Java y la clase Thread. Parte II — Sincronización Mejor juntos: Java y la clase Thread. Parte IV — Callable, Future y amigos Mejor juntos: Java y la clase Thread. Parte V — Ejecutor, ThreadPool, Fork/Join Mejor juntos: Java y la clase Thread. Parte VI — ¡Dispara!
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION