Interbloqueo de subprocesos de Java y Livelock

1. Información general

Si bien el subproceso múltiple ayuda a mejorar el rendimiento de una aplicación, también conlleva algunos problemas. En este tutorial, analizaremos dos de estos problemas, interbloqueo y bloqueo activo, con la ayuda de ejemplos de Java.

2. Punto muerto

2.1. ¿Qué es Deadlock?

Un interbloqueo se produce cuando dos o más subprocesos esperan eternamente un bloqueo o recurso retenido por otro de los subprocesos . En consecuencia, una aplicación puede detenerse o fallar porque los subprocesos bloqueados no pueden progresar.

El clásico problema de los filósofos gastronómicos demuestra muy bien los problemas de sincronización en un entorno de subprocesos múltiples y se utiliza a menudo como un ejemplo de punto muerto.

2.2. Ejemplo de interbloqueo

Primero, echemos un vistazo a un ejemplo simple de Java para comprender el punto muerto.

En este ejemplo, crearemos dos subprocesos, T1 y T2 . El subproceso T1 llama a la operación1 y el subproceso T2 llama a las operaciones .

Para completar sus operaciones, el hilo T1 necesita adquirir lock1 primero y luego lock2 , mientras que el hilo T2 necesita adquirir lock2 primero y luego lock1 . Entonces, básicamente, ambos hilos están tratando de adquirir los bloqueos en el orden opuesto.

Ahora, escribamos la clase DeadlockExample :

public class DeadlockExample { private Lock lock1 = new ReentrantLock(true); private Lock lock2 = new ReentrantLock(true); public static void main(String[] args) { DeadlockExample deadlock = new DeadlockExample(); new Thread(deadlock::operation1, "T1").start(); new Thread(deadlock::operation2, "T2").start(); } public void operation1() { lock1.lock(); print("lock1 acquired, waiting to acquire lock2."); sleep(50); lock2.lock(); print("lock2 acquired"); print("executing first operation."); lock2.unlock(); lock1.unlock(); } public void operation2() { lock2.lock(); print("lock2 acquired, waiting to acquire lock1."); sleep(50); lock1.lock(); print("lock1 acquired"); print("executing second operation."); lock1.unlock(); lock2.unlock(); } // helper methods }

Ejecutemos ahora este ejemplo de interbloqueo y observemos el resultado:

Thread T1: lock1 acquired, waiting to acquire lock2. Thread T2: lock2 acquired, waiting to acquire lock1.

Una vez que ejecutamos el programa, podemos ver que el programa resulta en un interbloqueo y nunca sale. El registro muestra que el subproceso T1 está esperando lock2 , que está retenido por el subproceso T2 . De manera similar, el hilo T2 está esperando el bloqueo1 , que está retenido por el hilo T1 .

2.3. Evitando el punto muerto

El interbloqueo es un problema de concurrencia común en Java. Por lo tanto, deberíamos diseñar una aplicación Java para evitar posibles condiciones de interbloqueo.

Para empezar, debemos evitar la necesidad de adquirir múltiples bloqueos para un hilo. Sin embargo, si un hilo necesita múltiples bloqueos, debemos asegurarnos de que cada hilo adquiera los bloqueos en el mismo orden, para evitar cualquier dependencia cíclica en la adquisición de bloqueos .

También podemos usar intentos de bloqueo cronometrados , como el método tryLock en la interfaz de bloqueo , para asegurarnos de que un hilo no se bloquee infinitamente si no puede adquirir un bloqueo.

3. Livelock

3.1. ¿Qué es Livelock?

Livelock es otro problema de concurrencia y es similar al interbloqueo. En livelock, dos o más hilos continúan transfiriendo estados entre sí en lugar de esperar infinitamente como vimos en el ejemplo de interbloqueo. En consecuencia, los subprocesos no pueden realizar sus respectivas tareas.

Un gran ejemplo de livelock es un sistema de mensajería en el que, cuando ocurre una excepción, el consumidor de mensajes revierte la transacción y vuelve a colocar el mensaje al principio de la cola. Luego, el mismo mensaje se lee repetidamente de la cola, solo para causar otra excepción y volver a colocarse en la cola. El consumidor nunca recogerá ningún otro mensaje de la cola.

3.2. Ejemplo de Livelock

Ahora, para demostrar la condición de bloqueo activo, tomaremos el mismo ejemplo de bloqueo que hemos discutido anteriormente. En este ejemplo también, el hilo T1 llama a la operación1 y el hilo T2 llama a la operación2 . Sin embargo, cambiaremos ligeramente la lógica de estas operaciones.

Ambos hilos necesitan dos candados para completar su trabajo. Cada hilo adquiere su primer bloqueo pero descubre que el segundo bloqueo no está disponible. Entonces, para permitir que el otro subproceso se complete primero, cada subproceso libera su primer bloqueo e intenta adquirir ambos bloqueos nuevamente.

Demostremos livelock con una clase LivelockExample :

public class LivelockExample { private Lock lock1 = new ReentrantLock(true); private Lock lock2 = new ReentrantLock(true); public static void main(String[] args) { LivelockExample livelock = new LivelockExample(); new Thread(livelock::operation1, "T1").start(); new Thread(livelock::operation2, "T2").start(); } public void operation1() { while (true) { tryLock(lock1, 50); print("lock1 acquired, trying to acquire lock2."); sleep(50); if (tryLock(lock2)) { print("lock2 acquired."); } else { print("cannot acquire lock2, releasing lock1."); lock1.unlock(); continue; } print("executing first operation."); break; } lock2.unlock(); lock1.unlock(); } public void operation2() { while (true) { tryLock(lock2, 50); print("lock2 acquired, trying to acquire lock1."); sleep(50); if (tryLock(lock1)) { print("lock1 acquired."); } else { print("cannot acquire lock1, releasing lock2."); lock2.unlock(); continue; } print("executing second operation."); break; } lock1.unlock(); lock2.unlock(); } // helper methods }

Ahora, ejecutemos este ejemplo:

Thread T1: lock1 acquired, trying to acquire lock2. Thread T2: lock2 acquired, trying to acquire lock1. Thread T1: cannot acquire lock2, releasing lock1. Thread T2: cannot acquire lock1, releasing lock2. Thread T2: lock2 acquired, trying to acquire lock1. Thread T1: lock1 acquired, trying to acquire lock2. Thread T1: cannot acquire lock2, releasing lock1. Thread T1: lock1 acquired, trying to acquire lock2. Thread T2: cannot acquire lock1, releasing lock2. ..

Como podemos ver en los registros, ambos hilos están adquiriendo y liberando bloqueos repetidamente. Debido a esto, ninguno de los subprocesos puede completar la operación.

3.3. Evitando Livelock

Para evitar un bloqueo en vivo, debemos investigar la condición que está causando el bloqueo en vivo y luego encontrar una solución en consecuencia.

Por ejemplo, si tenemos dos subprocesos que adquieren y liberan bloqueos repetidamente, lo que da como resultado un bloqueo en vivo, podemos diseñar el código para que los subprocesos vuelvan a intentar adquirir los bloqueos a intervalos aleatorios. Esto le dará a los subprocesos una oportunidad justa de adquirir los bloqueos que necesitan.

Otra forma de solucionar el problema de la vida en el ejemplo del sistema de mensajería que hemos discutido anteriormente es colocar los mensajes fallidos en una cola separada para su posterior procesamiento en lugar de volver a colocarlos en la misma cola.

4. Conclusión

En este tutorial, hemos discutido el bloqueo y el bloqueo activo. Además, hemos examinado ejemplos de Java para demostrar cada uno de estos problemas y hemos mencionado brevemente cómo podemos evitarlos.

Como siempre, el código completo utilizado en este ejemplo se puede encontrar en GitHub.