Ejemplo de interbloqueo en Java

Situación de interbloqueo en Java es una situación de programación donde dos o más hilos quedan bloqueados indefinidamente. La situación de interbloqueo en Java surge con al menos dos hilos y dos o más recursos. Aquí he escrito un programa simple que provocará un escenario de interbloqueo en Java y luego veremos cómo analizarlo.

Interbloqueo en Java

Echemos un vistazo a un programa simple donde crearé un interbloqueo en los hilos de Java.

package com.journaldev.threads;

public class ThreadDeadlock {

    public static void main(String[] args) throws InterruptedException {
        Object obj1 = new Object();
        Object obj2 = new Object();
        Object obj3 = new Object();
    
        Thread t1 = new Thread(new SyncThread(obj1, obj2), "t1");
        Thread t2 = new Thread(new SyncThread(obj2, obj3), "t2");
        Thread t3 = new Thread(new SyncThread(obj3, obj1), "t3");
        
        t1.start();
        Thread.sleep(5000);
        t2.start();
        Thread.sleep(5000);
        t3.start();
        
    }

}

class SyncThread implements Runnable{
    private Object obj1;
    private Object obj2;

    public SyncThread(Object o1, Object o2){
        this.obj1=o1;
        this.obj2=o2;
    }
    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        System.out.println(name + " acquiring lock on "+obj1);
        synchronized (obj1) {
         System.out.println(name + " acquired lock on "+obj1);
         work();
         System.out.println(name + " acquiring lock on "+obj2);
         synchronized (obj2) {
            System.out.println(name + " acquired lock on "+obj2);
            work();
        }
         System.out.println(name + " released lock on "+obj2);
        }
        System.out.println(name + " released lock on "+obj1);
        System.out.println(name + " finished execution.");
    }
    private void work() {
        try {
            Thread.sleep(30000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

En el programa anterior, SyncThread está implementando la interfaz Runnable y trabaja en dos objetos adquiriendo el bloqueo de cada uno de ellos uno por uno mediante un bloqueo sincronizado. En el método principal, tengo tres hilos ejecutándose para SyncThread y hay un recurso compartido entre cada uno de los hilos. Los hilos se ejecutan de tal manera que podrán adquirir el bloqueo en el primer objeto, pero cuando intentan adquirir el bloqueo en el segundo objeto, entran en un estado de espera porque ya está bloqueado por otro hilo. Esto forma una dependencia cíclica para el recurso entre los hilos, causando un interbloqueo. Cuando ejecuto el programa anterior, aquí está la salida generada, pero el programa nunca termina debido al interbloqueo en los hilos de Java.

t1 acquiring lock on java.lang.Object@6d9dd520
t1 acquired lock on java.lang.Object@6d9dd520
t2 acquiring lock on java.lang.Object@22aed3a5
t2 acquired lock on java.lang.Object@22aed3a5
t3 acquiring lock on java.lang.Object@218c2661
t3 acquired lock on java.lang.Object@218c2661
t1 acquiring lock on java.lang.Object@22aed3a5
t2 acquiring lock on java.lang.Object@218c2661
t3 acquiring lock on java.lang.Object@6d9dd520

Aquí podemos identificar claramente la situación de interbloqueo a partir de la salida, pero en aplicaciones de la vida real, es muy difícil encontrar la situación de interbloqueo y depurarla.

Cómo Detectar un Bloqueo en Java

Para detectar un bloqueo en Java, necesitamos observar el volcado de hilos de Java de la aplicación, en la publicación anterior expliqué cómo podemos generar un volcado de hilos utilizando el perfilador VisualVM o utilizando la utilidad jstack. Aquí está el volcado de hilos del programa anterior.

2012-12-27 19:08:34
Full thread dump Java HotSpot(TM) 64-Bit Server VM (23.5-b02 mixed mode):

"Attach Listener" daemon prio=5 tid=0x00007fb0a2814000 nid=0x4007 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"DestroyJavaVM" prio=5 tid=0x00007fb0a2801000 nid=0x1703 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"t3" prio=5 tid=0x00007fb0a204b000 nid=0x4d07 waiting for monitor entry [0x000000015d971000]
   java.lang.Thread.State: BLOCKED (on object monitor)
	at com.journaldev.threads.SyncThread.run(ThreadDeadlock.java:41)
	- waiting to lock <0x000000013df2f658> (a java.lang.Object)
	- locked <0x000000013df2f678> (a java.lang.Object)
	at java.lang.Thread.run(Thread.java:722)

"t2" prio=5 tid=0x00007fb0a1073000 nid=0x4207 waiting for monitor entry [0x000000015d209000]
   java.lang.Thread.State: BLOCKED (on object monitor)
	at com.journaldev.threads.SyncThread.run(ThreadDeadlock.java:41)
	- waiting to lock <0x000000013df2f678> (a java.lang.Object)
	- locked <0x000000013df2f668> (a java.lang.Object)
	at java.lang.Thread.run(Thread.java:722)

"t1" prio=5 tid=0x00007fb0a1072000 nid=0x5503 waiting for monitor entry [0x000000015d86e000]
   java.lang.Thread.State: BLOCKED (on object monitor)
	at com.journaldev.threads.SyncThread.run(ThreadDeadlock.java:41)
	- waiting to lock <0x000000013df2f668> (a java.lang.Object)
	- locked <0x000000013df2f658> (a java.lang.Object)
	at java.lang.Thread.run(Thread.java:722)

"Service Thread" daemon prio=5 tid=0x00007fb0a1038000 nid=0x5303 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread1" daemon prio=5 tid=0x00007fb0a1037000 nid=0x5203 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread0" daemon prio=5 tid=0x00007fb0a1016000 nid=0x5103 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Signal Dispatcher" daemon prio=5 tid=0x00007fb0a4003000 nid=0x5003 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Finalizer" daemon prio=5 tid=0x00007fb0a4800000 nid=0x3f03 in Object.wait() [0x000000015d0c0000]
   java.lang.Thread.State: WAITING (on object monitor)
	at java.lang.Object.wait(Native Method)
	- waiting on <0x000000013de75798> (a java.lang.ref.ReferenceQueue$Lock)
	at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:135)
	- locked <0x000000013de75798> (a java.lang.ref.ReferenceQueue$Lock)
	at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:151)
	at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:177)

"Reference Handler" daemon prio=5 tid=0x00007fb0a4002000 nid=0x3e03 in Object.wait() [0x000000015cfbd000]
   java.lang.Thread.State: WAITING (on object monitor)
	at java.lang.Object.wait(Native Method)
	- waiting on <0x000000013de75320> (a java.lang.ref.Reference$Lock)
	at java.lang.Object.wait(Object.java:503)
	at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:133)
	- locked <0x000000013de75320> (a java.lang.ref.Reference$Lock)

"VM Thread" prio=5 tid=0x00007fb0a2049800 nid=0x3d03 runnable 

"GC task thread#0 (ParallelGC)" prio=5 tid=0x00007fb0a300d800 nid=0x3503 runnable 

"GC task thread#1 (ParallelGC)" prio=5 tid=0x00007fb0a2001800 nid=0x3603 runnable 

"GC task thread#2 (ParallelGC)" prio=5 tid=0x00007fb0a2003800 nid=0x3703 runnable 

"GC task thread#3 (ParallelGC)" prio=5 tid=0x00007fb0a2004000 nid=0x3803 runnable 

"GC task thread#4 (ParallelGC)" prio=5 tid=0x00007fb0a2005000 nid=0x3903 runnable 

"GC task thread#5 (ParallelGC)" prio=5 tid=0x00007fb0a2005800 nid=0x3a03 runnable 

"GC task thread#6 (ParallelGC)" prio=5 tid=0x00007fb0a2006000 nid=0x3b03 runnable 

"GC task thread#7 (ParallelGC)" prio=5 tid=0x00007fb0a2006800 nid=0x3c03 runnable 

"VM Periodic Task Thread" prio=5 tid=0x00007fb0a1015000 nid=0x5403 waiting on condition 

JNI global references: 114


Found one Java-level deadlock:
=============================
"t3":
  waiting to lock monitor 0x00007fb0a1074b08 (object 0x000000013df2f658, a java.lang.Object),
  which is held by "t1"
"t1":
  waiting to lock monitor 0x00007fb0a1010f08 (object 0x000000013df2f668, a java.lang.Object),
  which is held by "t2"
"t2":
  waiting to lock monitor 0x00007fb0a1012360 (object 0x000000013df2f678, a java.lang.Object),
  which is held by "t3"

Java stack information for the threads listed above:
===================================================
"t3":
	at com.journaldev.threads.SyncThread.run(ThreadDeadlock.java:41)
	- waiting to lock <0x000000013df2f658> (a java.lang.Object)
	- locked <0x000000013df2f678> (a java.lang.Object)
	at java.lang.Thread.run(Thread.java:722)
"t1":
	at com.journaldev.threads.SyncThread.run(ThreadDeadlock.java:41)
	- waiting to lock <0x000000013df2f668> (a java.lang.Object)
	- locked <0x000000013df2f658> (a java.lang.Object)
	at java.lang.Thread.run(Thread.java:722)
"t2":
	at com.journaldev.threads.SyncThread.run(ThreadDeadlock.java:41)
	- waiting to lock <0x000000013df2f678> (a java.lang.Object)
	- locked <0x000000013df2f668> (a java.lang.Object)
	at java.lang.Thread.run(Thread.java:722)

Found 1 deadlock.

La salida del volcado de hilos muestra claramente la situación de bloqueo y los hilos y recursos involucrados que causan la situación de bloqueo. Para analizar el bloqueo, debemos buscar los hilos con estado BLOQUEADO y luego los recursos por los que está esperando bloquear. Cada recurso tiene una identificación única con la que podemos encontrar qué hilo ya tiene el bloqueo en el objeto. Por ejemplo, el hilo “t3” está esperando bloquear 0x000000013df2f658 pero ya está bloqueado por el hilo “t1”. Una vez que analizamos la situación de bloqueo y descubrimos los hilos que están causando el bloqueo, necesitamos realizar cambios en el código para evitar la situación de bloqueo.

Cómo evitar el bloqueo en Java

Estas son algunas pautas utilizando las cuales podemos evitar la mayoría de las situaciones de bloqueo.

  • Avoidar Bloqueos Anidados: Esta es la razón más común de los deadlocks, evita bloquear otro recurso si ya tienes uno. Es casi imposible encontrarse en una situación de deadlock si estás trabajando con solo un bloqueo de objeto. Por ejemplo, aquí hay otra implementación del método run() sin bloqueo anidado y el programa se ejecuta con éxito sin situación de deadlock.

        public void run() {
            String name = Thread.currentThread().getName();
            System.out.println(name + " adquiriendo bloqueo en " + obj1);
            synchronized (obj1) {
                System.out.println(name + " bloqueo adquirido en " + obj1);
                trabajo();
            }
            System.out.println(name + " liberando bloqueo en " + obj1);
            System.out.println(name + " adquiriendo bloqueo en " + obj2);
            synchronized (obj2) {
                System.out.println(name + " bloqueo adquirido en " + obj2);
                trabajo();
            }
            System.out.println(name + " liberando bloqueo en " + obj2);
    
            System.out.println(name + " finalizó la ejecución.");
        }
    
  • Bloquear solo lo necesario: Deberías adquirir el bloqueo solo en los recursos en los que tienes que trabajar, por ejemplo, en el programa anterior estoy bloqueando el recurso completo del objeto, pero si solo nos interesa uno de sus campos, entonces deberíamos bloquear solo ese campo específico, no el objeto completo.

  • Avoidar esperas indefinidas: Puedes entrar en un bloqueo si dos hilos están esperando indefinidamente que el otro termine utilizando join de hilo. Si tu hilo tiene que esperar a que otro hilo termine, siempre es mejor usar join con el tiempo máximo que deseas esperar para que el hilo termine.

Eso es todo para los bloqueos en los hilos de Java.

Source:
https://www.digitalocean.com/community/tutorials/deadlock-in-java-example