Search code examples
javamultithreadingsynchronizationsynchronizedsynchronous

Java guaranteed deadlock


I have two classes:

Deadlock1.java

class Client {
   final Object resource1 = "resource1";
   final Object resource2 = "resource2";
   void doS1() {
       synchronized(resource1) {} 
    }
    void doS2() {
       synchronized(resource2) {}
    }
 }

public class Deadlock1 {
  public static void main(String[] args) {
  Client client = new Client();
  new Thread(
      () ->
             {
               client.doS1();
               try {
                Thread.sleep(50);
              } catch (InterruptedException e) {
             }
              client.doS2();
      }).start();

     new Thread(
      () ->
             {
              client.doS2();
              try {
                Thread.sleep(50);
              } catch (InterruptedException e) {
            }
             client.doS1();
      }).start();
  }
}

and Deadlock2.java

class Client {
    final Object resource1 = "resource1";
    final Object resource2 = "resource2";  
}

public class Deadlock2{
  public static void main(String[] args) {
     Client client = new Client();

     new Thread(
      () ->
      {
        synchronized (client.resource1) {
        try {
            Thread.sleep(50);
          } catch (InterruptedException e) {
       }

       synchronized (client.resource2) {}
        }
      }).start();

     new Thread(
      () ->
             {
        synchronized (client.resource2) {   
          try {
            Thread.sleep(50);
          } catch (InterruptedException e) {
          }
              synchronized (client.resource1) {}
        }
      }).start();
  }
}

In Deadlock1 deadlock didn't happened but in Deadlock2 did. I don't understand why? And i do not quite understand the meaning of the concept of synchronized block. Why this block is part of thread code but not some common piece of code which different threads execute?


Solution

  • A synchronized block prevents simultaneous execution of code on the same monitor object. In this case of doS1() the monitor object is resource1 and in doS2() the monitor object is resource2. When a thread enters a synchronized block it attempts to acquire a lock on the monitor object. If it gets the lock it will proceed and release the lock when only when it exits the block (or it releases the lock). If it can't get the lock (because another thread has the lock already then the thread will block until the lock is released and it can acquire it).

    Your two examples above, Deadlock1 and Deadlock2, are not executing equivalent code. In Deadllock1 both monitor objects locks can't be acquired by the same the same thread at the same time.

    In Deadlock2 each thread it trying to acquire a lock on both monitor objects at the same time.

    1. Thread 1 acquires a lock on resource1 and sleeps for 50 ms
    2. At the same time Thread 2 acquires a lock on resource2 and sleeps for 50 ms
    3. When Thread 1 continues it still has a lock on resource1 and tries to acquire a lock on resource2. The resource2 lock is still held by Thread 2 so it blocks waiting for resource2 to be released.
    4. Thread 2 meanwhile, tries to acquire a lock on resource1, but this is still held by Thread 1 so it blocks waiting for Thread 1 to release the monitor for resource1.
    5. Now both threads are blocked waiting for a monitor object to be released and as they are both blocked they can't release the monitor object that they have locked... hence the deadlock.

    If we rewrite Deadlock1 to mimic the functionality of Deadlock2 so that it does produce a deadlock it would look like this:

    public class Deadlock1 {
        static class Client {
            final Object resource1 = "resource1";
            final Object resource2 = "resource2";
    
            void doS1() {
                synchronized (resource1) {
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                    }
                    doS2();
                }
            }
    
            void doS2() {
                synchronized (resource2) {
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                    }
                    doS1();
                }
            }
        }
    
        public static void main(String[] args) {
            Client client = new Client();
            new Thread(client::doS1).start();
            new Thread(client::doS2).start();
        }
    }
    

    Alternatively if we rewrite Deadlock2 to mimic the functionality of Deadlock1 so it doesn't produce a deadlock it would look like this:

    public class Deadlock2 {
        static class Client {
            final Object resource1 = "resource1";
            final Object resource2 = "resource2";
        }
    
        public static void main(String[] args) {
            Client client = new Client();
    
            new Thread(
                    () ->
                    {
                        synchronized (client.resource1) {
                            try {
                                Thread.sleep(50);
                            } catch (InterruptedException e) {
                            }
    
                        }
                        synchronized (client.resource2) {}
                    }).start();
    
            new Thread(
                    () ->
                    {
                        synchronized (client.resource2) {
                            try {
                                System.out.println("3");
                                Thread.sleep(50);
                            } catch (InterruptedException e) {
                            }
                        }
                        synchronized (client.resource1) {}
                    }).start();
        }
    }