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 LOCKINGand... 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