Google

Aug 20, 2012

Article: Scenarios and solutions for better concurrency and thread safety part-1 ReadWriteLocks

Scenario: You need to load  stock exchange security codes from a database and cache them for performance. The security codes need to be refreshed say every 30 minutes. This cached data needs to be populated and refreshed by a single writer thread and read by several reader threads. How will you ensure that your read/write solution is scalable and thread safe?

Solution:  The java.util.concurrent.locks package provides classes that implement read/write locks where the read lock can be executed in parallel by multiple threads and the write lock can be held by only a single thread. The ReadWriteLock interface maintains a pair of associated locks, one for read-only and one for writing. The readLock( ) may be held simultaneously by multiple reader threads, while the writeLock( ) is exclusive. In general, this implementation improves performance and scalability when compared to the mutex locks (i.e. via synchronized key word) when

  • there are more reads and read duration compared writes and write duration. 
  • It also depends on the system you are running on -- for example multi-core processors for better parallelism. 

The ConcurrentHashmap is another example where improved performance can be achieved when you have more reads than writes. The ConcurrentHashmap allows concurrent reads and locks only the buckets that are used to modify or insert data.

Here are a few terminlogies you need to be familiar with in implementing locks.

Q. What is a reentrant lock?
A. This can be considered as a replacement for the traditional “wait-notify” method. The basic concept is, every thread needs to acquire the lock before entering in to the critical section and should release it after finishing it. ReentrantLock eliminates the use of “synchronized” keyword to provide better concurrency

The term reentrant means if a thread enters a second synchronized block protected by a monitor that the thread already owns from , the thread will be allowed to proceed, and the lock will not be released when the thread exits the second (or subsequent) synchronized block, but only will be released when it exits the first synchronized block it entered protected by that monitor. Similarly, when you use reentrant locks, there will be an acquisition count associated with the lock, and if a thread that holds the lock acquires it again, the acquisition count is incremented and the lock then needs to be released twice to truly release the lock.  A writer can acquire the read lock - but the reverse is not true. If a reader tries to acquire the write lock it will never succeed.

Q. What do you need to watch out for in releasing a lock?
A. The locks need to be released in a finally block, otherwise if an exception is thrown, the lock might never get released.

Q. Why would you need a reentrant lock when there is synchronized keyword? How will you favor one over the other?
A. The reentrant locks are more scalable as it allows concurrent reads. Having said this, the locking classes in java.util.concurrent.lock package are advanced tools for advanced users and specific situations like the scenario discussed above. In general, you should stick with synchronization unless you have a specific need  like
  • the number of reads are far more than the number of writes. 
  • demonstrated evidence through proper performance testing that synchronization in a specific situation is a scalability bottleneck. 
  • specif features like timed lock waits, interruptible lock waits, non-block-structured locks, multiple condition variables, or lock polling are required.

Q. What are fair and unfair locks?
A. One of the arguments to the constructor of ReentrantLock is a boolean value that lets you choose whether you want a fair or an unfair lock. A fair lock is one where the threads acquire the lock in the same order they asked for it. In another words, when the write lock is released either the longest-waiting single writer will be assigned the write lock, or if there is a reader waiting longer than any writer, the set of readers will be assigned the read lock. When constructed as non-fair, the order of entry to the lock are not necessarily in the arrival order.
If the readers are active and a writer enters the lock then no subsequent readers will be granted the read lock until after that writer has acquired and released the write lock. Here is a simplified example of read/write locks.



Firstly, define the SharedData that gets accessed by multiple reader and writer threads. So, provide proper locks.

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.ReentrantReadWriteLock;


public class SharedData<T> {
 
 private List<Integer> numbers = new ArrayList<Integer>(20);
 private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

 public List<Integer> getTechnologies() {
  return numbers;
 }
 
 public void writeData(Integer number) {
  String threadName = Thread.currentThread().getName();
  lock.writeLock().lock(); //acquire a write lock if no other thread has acquired read/write lock
                           //only a single thread can write
  
  System.out.println("threads waiting for read/write lock: " + lock.getQueueLength());
  // This should be always one
  System.out.println("writer locks held " + lock.getWriteHoldCount());
  try {
   numbers.add(number);
   System.out.println(">>>>>" + threadName + " writing: " + number);
   Thread.sleep(2000);    // sleep for 2 seconds to demo read/write locks
  } catch (InterruptedException e) {
   e.printStackTrace();
  } finally {
   System.out.println(threadName + " releasing write lock");
   lock.writeLock().unlock();  //lock will be released
  }
 }
 
 public void readData() {
    String threadName = Thread.currentThread().getName();
    lock.readLock().lock();//acquire a read lock if no other thread has acquired a write lock.
                           //concurrent reads allowed.
    
    System.out.println("threads waiting for read/write lock: " + lock.getQueueLength());
    System.out.println("reader locks held "  + lock.getReadLockCount());
    try {
   for (Integer num: numbers) {
    System.out.println("<<<<<<<" + threadName + " reading: " + num);
   } 
      Thread.sleep(1000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    finally{
     System.out.println(threadName + " releasing read lock");
     lock.readLock().unlock();  //lock will be released
    }
    
  }
}


Next, define the reader and writer threads.

public class Reader<T> extends Thread {
 
 private SharedData<T> sharedData;
 
 public Reader(SharedData<T> sharedData) {
  this.sharedData = sharedData;
 }

 @Override
 public void run() {
  sharedData.readData();
 }
}


public class Writer<T> extends Thread {
 
 private boolean oddNumber = true;
 private SharedData<T> sharedData;
 
 public Writer(SharedData<T> sharedData, boolean oddNumber ) {
  this.sharedData = sharedData;
  this.oddNumber = oddNumber;
 }

 @Override
 public void run() {
  for(int i=1; i<=2; i++) {
   if(!oddNumber && i%2 == 0) {
      sharedData.writeData(i);
   }
   else if (oddNumber && !(i%2 == 0)){
      sharedData.writeData(i); 
   }
  }
 }
}


Finally, the ReadWriteProcessor class that spawns thw worker read and write threads and pass the SharedData to the threads.

public class ReadWriteProcessor {
 
 public static void main(String[] args) throws Exception {
  
  SharedData<Integer> data = new SharedData<Integer>();
  
  //create some writer worker threads
  Writer<Integer> oddWriter = new Writer<Integer>(data, true);
  oddWriter.setName("oddWriter");
  Writer<Integer> evenWriter = new Writer<Integer>(data, false);
  evenWriter.setName("evenWriter");
  
  //create some reader worker threads
  Reader<Integer> reader1 = new Reader<Integer>(data);
  reader1.setName("reader1");
  Reader<Integer> reader2 = new Reader<Integer>(data);
  reader2.setName("reader2");
  Reader<Integer> reader3 = new Reader<Integer>(data);
  reader3.setName("reader3");
  
  //start the writer threads
  oddWriter.start();
  Thread.sleep(100);
  evenWriter.start();
  
  //start the reader threads
  reader1.start(); 
  reader2.start();
  reader3.start();
 }
}


The output will be something like:

threads waiting for read/write lock: 0
writer locks held 1
>>>>>oddWriter writing: 1
oddWriter releasing write lock
threads waiting for read/write lock: 3
writer locks held 1
>>>>>evenWriter writing: 2
evenWriter releasing write lock
threads waiting for read/write lock: 2
threads waiting for read/write lock: 1
reader locks held 3
threads waiting for read/write lock: 0
reader locks held 3
reader locks held 2
<<<<<<<reader1 reading: 1
<<<<<<<reader2 reading: 1
<<<<<<<reader3 reading: 1
<<<<<<<reader2 reading: 2
<<<<<<<reader1 reading: 2
<<<<<<<reader3 reading: 2
reader1 releasing read lock
reader3 releasing read lock
reader2 releasing read lock


There are other real life scenarios where the java.util.concurrent package with the following classes can be put to great use.
  • CountDownLatch
  • CyclicBarrier
  • Semaphore
  • Atomic classes

The real life scenarios with solution will be discussed in the ensuing parts.

Labels:

0 Comments:

Post a Comment

Subscribe to Post Comments [Atom]

<< Home