Tuesday 11 October 2016

ReentrantLock Tutorial

We are living in an environment, where systems with multiprocessors is very common. To leverage these multi processors, application should use multiple threads. Threads are used to perform parallel tasks. While dealing with shared resources, you should restrict the access such that at most one thread can able to access these shared resources and the changes done by the thread must be visible to other threads. In simple terms, application should maintain following two properties.
a.   Atomicity
b.   Visibility

What is atomicity?
Only one thread can able to access the shared resources at a time. You can achieve this by using synchronization.

What is Visibility?
Updated shared data from one thread are available to other thread when it enters a synchronized block protected by that same monitor (lock).

Let me give an example, suppose there are two threads competing for resources, first thread should get resource1, second thread should get resource2, first thread should get resource3, second thread should get resource 4……

(T1, R1), (T2, R2), (T1, R3), (T2, R4), (T1, R5), (T2, R6)………

(T1, R1) means Thread1 gets resource1


Without synchronization, it is not possible to allocate resources like above. Following application demonstrate the above scenario.
public class ResourceAllocator {

 private static Object resource = new Object();
 private static boolean thread1Wait = false;
 private static int counter = 1;

 public static void main(String args[]) {

  Thread t1 = new Thread() {
   public void run() {
    synchronized (resource) {
     for (int i = 0; i < 10; i++) {
      if (thread1Wait) {
       try {
        resource.wait();
       } catch (InterruptedException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
       }
      }

      System.out.println("(T1, R" + counter + ")");
      counter++;
      thread1Wait = !thread1Wait;
      resource.notifyAll();
     }

    }
   }
  };

  Thread t2 = new Thread() {
   public void run() {
    synchronized (resource) {
     for (int i = 0; i < 10; i++) {
      if (!thread1Wait) {
       try {
        resource.wait();
       } catch (InterruptedException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
       }
      }

      System.out.println("(T2, R" + counter + ")");
      counter++;
      thread1Wait = !thread1Wait;
      resource.notifyAll();
     }

    }
   }
  };

  t1.start();
  t2.start();
 }
}


Let me briefly explain above program.
Thread t1 = new Thread() {
 public void run() {
  synchronized (resource) {
   for (int i = 0; i < 10; i++) {
    if (thread1Wait) {
     try {
      resource.wait();
     } catch (InterruptedException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
     }
    }

    System.out.println("(T1, R" + counter + ")");
    counter++;
    thread1Wait = !thread1Wait;
    resource.notifyAll();
   }

  }
 }
};

Thread1 verifies the flag, thread1Wait, if it is set to true, it release the lock on the resource by calling wait method. Else it takes the resource and increment the counter, invert the flag thread1Wait, and notifies other threads that are waiting for this resource.

ReentrantLock
Anything you can do with synchronized keyword, you can also do with ReentrantLock but not vice-versa.  ReentrantLock provides lock and unlock methods to acquire and release the locks.
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;

public class Task implements Runnable{
    
   int taskNum;
   static Lock myLock = new ReentrantLock();
   
   Task(int taskNum){
       this.taskNum = taskNum;
   }
    
   @Override
   public void run(){
       myLock.lock();
       try {
           System.out.println(taskNum +" Started ");
           try {
               TimeUnit.SECONDS.sleep(1);
           } catch (InterruptedException ex) {
               
           }
           System.out.println(taskNum +" Finished ");
        }
       finally {
          myLock.unlock();
       }
    }
}

public class LockDemo {
    public static void main(String args[]){
        int taskNum = 1;
        
        while(taskNum < 50){
            Task task = new Task(taskNum);
            new Thread(task).start();
            taskNum++;
        }
    }
}


Output
1 Started 
1 Finished 
2 Started 
2 Finished 
3 Started 
3 Finished 
4 Started 
4 Finished 
6 Started 
6 Finished 
5 Started 
5 Finished 
…
…
…

Reentrant class provides following constructors to instantiate.

public ReentrantLock()
public ReentrantLock(boolean fair)

The constructor for this class accepts an optional fairness parameter. When set true, under contention, locks favor granting access to the longest-waiting thread. Otherwise this lock does not guarantee any particular access order. Default constructor sets the fairness to false, to improveperformance.

Following table summarizes all the methods of ReentrantLock class.

Method
Description
public void lock()
Acquires the lock and set the lock count to 1, if this lock is not hold by any other thread.

If the lock is already hold by another thread,  then current thread becomes disabled for thread scheduling purposes and lies dormant until the lock has been acquired

If the current thread already holds the lock, then the lock count is incremented by 1, method returns immediately.
public void lockInterruptibly() throws InterruptedException
It is same as lock method, but If the lock is held by another thread then the current thread becomes disabled for thread scheduling purposes and lies dormant until one of two things happens:

a.   The lock is acquired by the current thread; or
b.   Some other thread interrupts the current thread.
public boolean tryLock()
Acquires the lock only if it is not held by another thread at the time of invocation. It returns true if the lock was free and was acquired by the current thread, or the lock was already held by the current thread; and false otherwise
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException
Acquires the lock if it is not held by another thread within the given waiting time and the current thread has not been interrupted.

If the lock is held by another thread then the current thread becomes disabled for thread scheduling purposes and lies dormant until one of three things happens:

a.   The lock is acquired by the current thread; or
b.   Some other thread interrupts the current thread; or
c.    The specified waiting time elapses
public void unlock()
If the current thread is the holder of this lock then the hold count is decremented. If the hold count is now zero then the lock is released. If the current thread is not the holder of this lock then IllegalMonitorStateException is thrown.
public Condition newCondition()
Returns a Condition instance for use with this Lock instance. Condition object supports wait, notify and notifyAll functionalities of Object class by providing await, signal, signalAll methods.

When the condition waiting methods are called the lock is released and, before they return, the lock is reacquired and the lock hold count restored to what it was when the method was called.
public int getHoldCount()
Return the number of holds on this lock by the current thread, or zero if this lock is not held by the current thread
public boolean isHeldByCurrentThread()
Return true if current thread holds this lock and false otherwise
public boolean isLocked()
Return true if any thread holds this lock and false otherwise
public final boolean isFair()
Return true if this lock has fairness set true
public final boolean hasQueuedThreads()
Return true if there may be other threads waiting to acquire the lock
public final boolean hasQueuedThread(Thread thread)
Return true if the given thread is queued waiting for this lock
public final int getQueueLength()
Return the estimated number of threads waiting for this lock
public boolean hasWaiters(Condition condition)
Return true if there are any waiting threads on the given condition associated with this lock.
public int getWaitQueueLength(Condition condition)
Return the estimated number of waiting threads on the given condition associated with this lock

In addition to above public methods, ReentrantLock class provides following protected methods, sub classes can override these methods on need basis.

Method
Description
protected Thread getOwner()
Returns the thread that currently owns this lock, or null if not owned.
protected Collection<Thread> getQueuedThreads()
Returns a collection containing threads that may be waiting to acquire this lock.
protected Collection<Thread> getWaitingThreads(Condition condition)
Returns a collection containing those threads that may be waiting on the given condition associated with this lock.


Now let’s rewrite our resource allocation problem using ReentrantLock and Condition object.
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ResourceAllocator {

 private static Lock reentrantLock = new ReentrantLock();
 final static Condition waitCondition = reentrantLock.newCondition();
 private static boolean thread1Wait = false;
 private static int counter = 1;

 public static void main(String args[]) {

  Thread t1 = new Thread() {
   public void run() {
    try {
     reentrantLock.lock();
     for (int i = 0; i < 10; i++) {
      if (thread1Wait) {
       try {
        waitCondition.await();
       } catch (InterruptedException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
       }

      }

      System.out.println("(T1, R" + counter + ")");
      counter++;
      thread1Wait = !thread1Wait;
      waitCondition.signalAll();
     }

    } finally {
     reentrantLock.unlock();
    }
   }
  };

  Thread t2 = new Thread() {
   public void run() {
    try {
     reentrantLock.lock();
     for (int i = 0; i < 10; i++) {
      if (!thread1Wait) {
       try {
        waitCondition.await();
       } catch (InterruptedException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
       }

      }

      System.out.println("(T2, R" + counter + ")");
      counter++;
      thread1Wait = !thread1Wait;
      waitCondition.signalAll();
     }

    } finally {
     reentrantLock.unlock();
    }
   }
  };

  t1.start();
  t2.start();
 }
}


Output
(T1, R1)
(T2, R2)
(T1, R3)
(T2, R4)
(T1, R5)
(T2, R6)
(T1, R7)
(T2, R8)
(T1, R9)
(T2, R10)
(T1, R11)
(T2, R12)
(T1, R13)
(T2, R14)
(T1, R15)
(T2, R16)
(T1, R17)
(T2, R18)
(T1, R19)
(T2, R20)


private static Lock reentrantLock = new ReentrantLock();
Above statement instantiate ReentrantLock object.

Final static Condition waitCondition = reentrantLock.newCondition();
Above statement gets the new condition object on this lock. You can create any number of condition objects for a lock instance. By using condition object, you can release the lock by calling await method and notify other threads that are waiting on given condition objects etc.,
Thread t1 = new Thread() {
 public void run() {
  try {
   reentrantLock.lock();
   for (int i = 0; i < 10; i++) {
    if (thread1Wait) {
     try {
      waitCondition.await();
     } catch (InterruptedException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
     }

    }

    System.out.println("(T1, R" + counter + ")");
    counter++;
    thread1Wait = !thread1Wait;
    waitCondition.signalAll();
   }

  } finally {
   reentrantLock.unlock();
  }
 }
};


Make sure you call the unlock method in finally block. Since if you don’t call unlock method on given lock instance, other threads can’t access the shared data hold by given lock. Since finally block always executes, call unlock inside finally block.

How the lock hold count related to lock release?
When a thread acquires a lock, the acquisition count associated with the lock is set to 1. If the same thread acquire the lock again, the acquisition count is incremented and the lock then needs to be released twice to truly release the lock. Java supports maximum of 2147483647 recursive locks by the same thread. Attempts to exceed this limit result in Error throws from locking methods.

Why should we release the lock in finally block?
The lock must be released in finally block, otherwise, if the protected code throws an exception, the lock might never be released. Always follow the following template while working with locks.
Lock lock = new ReentrantLock();

lock.lock();
try { 
  // Shared Data to work with
}
finally {
  lock.unlock(); 
}

What is fair lock?
A fair lock is one where the threads acquire the lock in the same order they asked for it. In unfair lock, a thread can sometimes acquire a lock before another thread that asked for it first. Unfair locking works efficient than fair locking, always use unfair locking, unless your application forced you to use fair locking.

Synchronization Vs ReentrantLock
a.   It is not possible to interrupt a thread that is waiting to acquire a lock, whereas by using ReentrantLock, you can wait for some period of time to acquire a lock, if thread is unable to get the lock in given period of time, it can come out.
b.   The lock must be released in finally block, otherwise, if the protected code throws an exception, the lock might never be released. You must takes care of releasing the locks properly. In case of synchronization, JVM takes care of releasing the locks.
c.    A ReentrantLock is unstructured, unlike synchronized constructs -- i.e. you don't need to use a block structure for locking and can even hold a lock across methods.

Ex:
private ReentrantLock lock = new ReentrantLock();

public void foo() {
  ...
  lock.lock();
  ...
}

public void bar() {
  ...
  lock.unlock();
  ...
}

d.   Under high contention, Reentrant lock is better than synchronized construct. Go through following article for more information.

e.   You can get list of threads that are waiting on given lock, it is not possible using synchronization.
f.    ReentrantLock provide method to check whether a lock is being hold by any thread or not.
g.   By using ReentrantLock, you can try for a lock without blocking. But same is not possible in synchronization.

You may like


No comments:

Post a Comment