Sunday, May 5, 2013

Simplification with intrinsic locking

    When we deal with concurrency we use 'synchronized' keyword or we leverage the power of java.util.concurrent (JUC) stuff in order to tame our threads. Both of the methods have advantages and disadvantages. I would like to focus on one particular advantage of intrinsic locking.
Intrinsic locking can be utilized by means of the 'synchronized' keyword. This type of locking is reentrant which means that once the lock is acquired by particular thread all of the code guarded by the acquired lock is accessible by that thread (e.g. we have two synchronized instance methods in our class and the second method is invoked within the first method - the thread can pass through the invocations of the first and the second method fluently - lock can be reacquired). The equivalent of the intrinsic lock in JUC is ReentrantLock. Let's skim through the following piece of code.
package test;

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

public class NestedLocksExample {

    public static void main(String[] args) {
        final NestedLocksExample example = new NestedLocksExample();
       
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println("INTRINSIC LOCKING");
                    example.method1();
                } catch (Throwable t) { System.out.println("Expected exception"); }               
            }
        }).start();
       
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println("INTRINSIC LOCKING");
                    example.method1();
                } catch (Throwable t) { System.out.println("Expected exception"); }               
            }
        }).start();
       
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println("JUC LOCKING");
                    example.method2();
                } catch (Throwable t) { System.out.println("Expected exception"); }               
            }
        }).start();
       
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println("JUC LOCKING");
                    example.method2();
                } catch (Throwable t) { System.out.println("Expected exception"); }               
            }
        }).start();
    }
   
    private final Object monitor1 = new Object();
    private final Object monitor2 = new Object();
   
    private void method1() {
        synchronized (monitor1) {
            System.out.println("monitor1 acquired");
            synchronized (monitor2) {
                System.out.println("monitor2 acquired");
                int a = 0/0; // exception
            }
            System.out.println("monitor2 released");
        }
        System.out.println("monitor2 released");
    }
   
    private final Lock lock1 = new ReentrantLock();
    private final Lock lock2 = new ReentrantLock();
   
    private void method2() {
        lock1.lock();
        System.out.println("lock1 acquired");
            lock2.lock();
                System.out.println("lock2 acquired");
                int a = 0/0; // exception
            lock2.unlock();
            System.out.println("lock2 released");
        lock1.unlock();
        System.out.println("lock1 released");       
    }
}

I decided to use nested locks to make the example less trivial. The effect of execution (can be different because of the thread scheduler, but the general idea is the same):
INTRINSIC LOCKING
monitor1 acquired
monitor2 acquired
Expected exception
INTRINSIC LOCKING
monitor1 acquired
monitor2 acquired
Expected exception
JUC LOCKING
lock1 acquired
lock2 acquired
Expected exception
JUC LOCKING
and... our program hungs. There is nothing wrong with 'method1()' but 'method2()' is very dangerous. The locks are not released because of the exception. JStack reveals everything:
"Thread-3" prio=6 tid=0x000000000b990800 nid=0x16c0 waiting on condition [0x0000
00000d5ef000]
   java.lang.Thread.State: WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        - parking to wait for  <0x00000007d58544b0> (a java.util.concurrent.lock
s.ReentrantLock$NonfairSync)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:186)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInt
errupt(AbstractQueuedSynchronizer.java:834)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(A
bstractQueuedSynchronizer.java:867)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(Abstrac
tQueuedSynchronizer.java:1197)
        at java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLo
ck.java:214)
        at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:290)
...
The thread will wait forever and it is similar to deadlock. The cure for 'method2()' is 'finally' section:
private void method2() {
  lock1.lock();
  System.out.println("lock1 acquired");
  lock2.lock();
  System.out.println("lock2 acquired");
  try {
   // some stuff
   int a = 0 / 0; // exception
  } finally {
   lock2.unlock();
   System.out.println("lock2 released");
   lock1.unlock();
   System.out.println("lock1 acquired");
  }
 }
When we use intrinsic locking we do not need to remember about releasing the lock in case of the exception. All of the locks (even nested locks) are released automatically. However when we use JUC ReentrantLock we have to remember about releasing locks in 'finally' section.

No comments :

Post a Comment