Pages

Friday 7 February 2014

ReentrantReadWriteLock in java


  • ReentrantLock which is explained here is efficient is many scenarios. However they follow a conservative locking strategy that prevents writer/writer and writer/reader overlap, but also prevents reader/reader overlap.
  • In many cases you want to allow multiple Threads to have simultaneous read without locking each other. In this case using ReentrantLock can have a huge performance overhead.
  • As long as each thread is guaranteed an up to date view of the data and no other thread modifies the data while the readers are viewing it, there will be no  problems.
A ReentrantReadWriteLock allow a resource can be accessed by multiple readers or a single writer at a time, but not both.

  • ReentrantReadWriteLock provides two Lock objects, one for reading and one for writing. To read data guarded by a ReentrantReadWriteLock you must first acquire the read lock, and to modify data guarded by a ReentrantReadWriteLock you must  first acquire the  write  lock. While there may appear to be two separate  locks, the read  lock and  write  lock are simply different views of an integrated read write lock object.
  • When a thread is doing a write operation, there can't be any thread doing read operations.
You should read the Java doc regarding the ReentrantReadWriteLock properties: http://docs.oracle.com/javase/6/docs/api/java/util/concurrent/locks/ReentrantReadWriteLock.html

ReentrantReadWriteLock Locking Rules

  • When the write lock is acquired, no other threads can acquire the lock in any form, whereas with a reader lock any other thread can acquire the read lock if it wants to.
  • Non Fair: The order in which threads are granted access is unspecified
  • Fair:
    • Trying to acquire a Read Lock: Will block if either the write lock is held, or there is a waiting writer thread.
    • Trying to acquire a Write Lock: Will block unless both the read lock and write lock are free.
Examples:


public class RWDictionary {
 private final Map<String, Data> m = new TreeMap<String, Data>();
    private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    private final Lock r = rwl.readLock();
    private final Lock w = rwl.writeLock();

    public Data get(String key) {
        r.lock();
        try { return m.get(key); }
        finally { r.unlock(); }
    }
    public String[] allKeys() {
        r.lock();
        try { return (String[]) m.keySet().toArray(); }
        finally { r.unlock(); }
    }
    public Data put(String key, Data value) {
        w.lock();
        try { return m.put(key, value); }
        finally { w.unlock(); }
    }
    public void clear() {
        w.lock();
        try { m.clear(); }
        finally { w.unlock(); }
    }
}

In the above example we are using ReentrantReadWriteLock for concurrent access to the TreeMap. Multiple threads should be able to read & iterate over the Map simultaneously and should lock when we are performing any put operations on the Map.

Lock Downgrading

 One of the properties of the ReentrantReadWriteLock is the downgrading of the Locks. Which allows downgrading from the write lock to a read lock, by acquiring the write lock, then the read lock and then releasing the write lock. However, upgrading from a read lock to the write lock is not possible.

 Example:


public class CachedData {
 Object data;
 volatile boolean cacheValid;
 ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

 void processCachedData() {
  rwl.readLock().lock();
  if (!cacheValid) {
   // Must release read lock before acquiring write lock
   rwl.readLock().unlock();
   rwl.writeLock().lock();

   // Some processing
   
   // Downgrade by acquiring read lock before releasing write lock
   rwl.readLock().lock();
   rwl.writeLock().unlock(); // Unlock write, still hold read
  }
  rwl.readLock().unlock();
   }
}

Here, "downgrading" the lock means that if you hold the write lock, you can switch down to holding just the read lock by acquiring the read lock, then releasing the write lock. This means that you can have a thread that starts off doing something critically important (something that would prevent other threads from reading), does its work, and then switches to the lower-priority lock (the read lock) without ever being without the lock. This allows you to hold the lock continuously without getting preempted.



Reentrant Lock in Java


- ReentrantLock in Java is added on java.util.concurrent package in Java 1.5 along with other concurrent utilities.
- ReentrantLock implements Lock, providing the same mutual exclusion and memory visibility guarantees as synchronized.
- Acquiring a ReentrantLock has the same memory semantics as entering a synchronized block, and releasing a ReentrantLock has the same memory semantics as exiting a synchronized block.
- And like synchronized,ReentrantLock offers reentrant locking semantics i.e. it allows a thread to recursively acquire the same lock that it is holding.

Extended capabilities include:

  • Non block structured Locking:  With intrinsic locks, acquire release pairs are block structured. A lock is always released in the same basic block in which it  was  acquired,  regardless  of  how  control  exits  the  block. However in reentrant lock you can acquire the lock in one method and release it in some other method.
  • The ability to lock interruptibly: The lockInterruptibly method allows you to try to acquire a lock while  remaining responsive to interruption. lockInterruptibly() may block if the lock is already held by another thread and will wait until the lock is acquired. This is the same as with regular lock(). But if another thread interrupts the waiting thread lockInterruptibly() will throw InterruptedException.
  • The ability to have more than one condition variable per monitor. Monitors that use the synchronized keyword can only have one. This means reentrant locks support more than one wait()/notify() queue.
  • Fairness: 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. Programs using fair locks accessed by many threads may display lower overall throughput (i.e., are slower; often much slower) than those using the default setting, but have smaller variances in times to obtain locks and guarantee lack of starvation. Synchronized blocks are unfair.
  • Polled and Timed Lock Acquisition: ReentrantLock provides convenient tryLock() method, which acquires lock only if its available or not held by any other thread. An overloaded method of tryLock takes the time it should wait and try acquiring the lock before exiting.

 Examples:

  1. Guarding Object State Using ReentrantLock. 

    void lockTest() {
     Lock lock = new ReentrantLock();
     lock.lock();
     try {
      // update object state
      // catch exceptions and restore invariants if necessary
     } finally {
      lock.unlock();
     }
    }
    

    This code is somewhat more complicated than using intrinsic locks. The lock must be released in a finally block. Otherwise, the lock would never be released if the guarded code were to throw an exception.

  2. Polled and Timed Lock acquisition

    package com.concurrency;
    
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    public class ReentrantLockingDemo {
     final Lock lock = new ReentrantLock();
    
     public static void main(final String... args) {
      new ReentrantLockingDemo().go();
     }
    
     private void go() {
      new Thread(newRunable(), "Thread1").start();
      new Thread(newRunable(), "Thread2").start();
     }
    
     private Runnable newRunable() {
      return new Runnable() {
    
       @Override
       public void run() {
        do {
         try {
    
          if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
           try {
    
            System.out.println("locked thread " + Thread.currentThread().getName());
            Thread.sleep(1000);
           } finally {
            lock.unlock();
            System.out.println("unlocked locked thread " + Thread.currentThread().getName());
           }
           break;
          } else {
           System.out.println("unable to lock thread " + Thread.currentThread().getName() + " will re try again");
          }
         } catch (InterruptedException e) {
          e.printStackTrace();
         }
        } while (true);
       }
      };
     }
    }
    

    Output:
    locked thread Thread1
    unable to lock thread Thread2 will re try again
    unlocked locked thread Thread1
    locked thread Thread2
    unlocked locked thread Thread2

  3. Interruptible Lock Acquisition.

    package com.concurrency;
    
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    public class ReentrantLockInterruptiblyDemo {
     private Lock lock = new ReentrantLock();
     public ReentrantLockInterruptiblyDemo() {
      lock.lock();
     }
     
      public void f() {
       try {
        // This will never be available to a second task
        lock.lockInterruptibly(); // Special call
        System.out.println("lock acquired in f()");
       } catch(InterruptedException e) {
        System.out.println("Interrupted from lock acquisition in f()");
       }
      }
      public static void main(String[] args) throws Exception {
      Thread t = new Thread(new Blocked2());
      t.start();
      TimeUnit.SECONDS.sleep(1);
      System.out.println("Issuing t.interrupt()");
      t.interrupt();
     }
    }
    
    class Blocked2 implements Runnable {
     ReentrantLockInterruptiblyDemo blocked = new ReentrantLockInterruptiblyDemo();
     public void run() {
      System.out.println("Waiting for f() in BlockedMutex");
      blocked.f();
      System.out.println("Broken out of blocked call");
     }
    }
     
    

    Output:
    Waiting for f() in BlockedMutex
    Issuing t.interrupt()
    Interrupted from lock acquisition in f()
    Broken out of blocked call

  4. Using conditions in ReentrantLock.

    This Example is taken from http://docs.oracle.com/javase/1.5.0/docs/api/java/util/concurrent/locks/Condition.html

    We have a bounded buffer which supports put and take methods. If a take is attempted on an empty buffer, then the thread will block until an item becomes available; if a put is attempted on a full buffer, then the thread will block until a space becomes available. We would like to keep waiting put threads and take threads in separate wait-sets so that we can use the optimization of only notifying a single thread at a time when items or spaces become available in the buffer. This can be achieved using two Condition instances.

    public class BoundedBuffer {
     final Lock lock = new ReentrantLock();
     final Condition notFull = lock.newCondition();
     final Condition notEmpty = lock.newCondition();
    
     final Object[] items = new Object[100];
     int putptr, takeptr, count;
    
     public void put(Object x) throws InterruptedException {
      lock.lock();
      try {
       while (count == items.length)
        notFull.await();
       items[putptr] = x;
       if (++putptr == items.length)
        putptr = 0;
       ++count;
       notEmpty.signal();
      } finally {
       lock.unlock();
      }
     }
    
     public Object take() throws InterruptedException {
      lock.lock();
      try {
       while (count == 0)
        notEmpty.await();
       Object x = items[takeptr];
       if (++takeptr == items.length)
        takeptr = 0;
       --count;
       notFull.signal();
       return x;
      } finally {
       lock.unlock();
      }
     }
    }