java.util.concurrent.locks: Java 5 Concurrency Locks

Before the arrival of Java 5, controlling access to shared data in the JVM memory was straightforward. You just had to channel all concurrent access to the shared data via a synchronized method or synchronized block of code. All this changed with the advent of Java 5. Java 5 unleashed a series of disparate fine grained concurrency utility classes which gave the developer a never before available control over concurrent access. This post will limit itself to focusing on the Lock interface and its implementations.

Before diving into the Lock interface semantics, let’s do a brief recap of the pre Java 5 days. Consider the following class Data.

public class Data {
        private static Data data = new Data();
	private Data() {
	}
	public static Data get() {
		return data;
	}
	public void process(String name) {
		synchronized (this) {
			DateFormat df = new SimpleDateFormat("HH:mm:ss");
			System.out.println(df.format(new java.util.Date())
				+ " Running thread: " + name);
			long startTime = System.nanoTime();
			try {
				Thread.sleep(10000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			long endTime = System.nanoTime();
			System.out.println(df.format(new java.util.Date()) +
				" Time spent in thread: "
				+(float)(endTime-startTime)/1000000000 +/>
				" secs.");
		}
	}
}

Let’s assume that the Data class encapsulates some information which is accessible as shared data across the application. Access to shared data needs to undertaken in a controlled manner to ensure data integrity. The Data class represents a typical way for storing shared data in memory. The Data class is implemented as a singleton per JVM, refer the private constructor and the instantiation of the static reference variable data as a static member variable of the Data class. A static method get is available to gain access to the singleton Data instance. Conventionally, the Data class would have additional method(s) to manipulate shared data. For our purposes I am treating the Data instance as shared data, the process method is a utility method to manipulate the shared data. The process method provides a synchronized block for access to Data instance. For illustration purposes the process method purely gains exclusive access on Data instance, sleeps for 10 secs and then exits, thereby releasing the lock.

To simulate concurrent access, I have created the DataAccessThread class

public class DataAccessThread implements Runnable {
	private String name = null;
	public DataAccessThread(String threadName) {
		this.name = threadName;
	}
	public void run() {
		Data data = Data.get();
		data.process(this.name);
	}
}
And to validate please find below my test class:
public class SharedAccessTest {
	public static void main(String[] args) {
		DataAccessThread thread1 = new DataAccessThread("Number 1");
		DataAccessThread thread2 = new DataAccessThread("Number 2");
		//Run the threads
		Thread thd1 = new Thread(thread1);
		Thread thd2 = new Thread(thread2);
		thd1.start();
		thd2.start();
	}
}

On running the test class, I get the following results:

17:57:06 Running thread: Number 2
17:57:16 Time spent in thread: 10.000185 secs.
17:57:16 Running thread: Number 1
17:57:26 Time spent in thread: 9.999647 secs.

As expected one of the two threads gains exclusive access to the Data instance, sleeps for 10 secs and then allows the other thread to gain access of Data and run successfully and exit. This approach is fine, however there are some areas of concerns, namely:

Lock contention: What if the process method takes a considerable time to process data? In such a scenario, the other thread would be waiting for a long time for the lock to be released during which the operation could timeout?
Read-Write Access Control: Also there is no differentiation between read and write operations. What if the thread awaiting control wants to access the data in a read only fashion. Synchronized does not handle that.
Lock Status Awareness: There is no ready-to-use mechanism for the developer to identify if a lock is already taken or to programmatically try and acquire lock for a specific duration.

To get around all these problems, Java 5 introduced the concept of a lock. The Lock interface has three concrete implementations ReentrantLock, ReentrantReadWriteLock.ReadLock and ReentrantReadWriteLock.WriteLock. Let’s first focus on ReentrantLock.

Let’s consider the simple example of a counter which can be incremented concurrently. Note a concurrently accessed counter can be effectively handled with AtomicInteger as done in a prior post. Here the counter is used just to demonstrate the use of the Java 5 Lock paradigm.

The LockData class encapsulates the counter variable and is responsible for controlling and sequencing concurrent access.

public class LockData {

	private Lock lock = new ReentrantLock();
	private static LockData lockData = new LockData();
	private int counter = 0;

	private LockData() {

	}

	public static LockData getInstance() {
		return lockData;
	}

	public int getCount() {
		return this.counter;
	}

	public void incrementCount() {
		DateFormat df = new SimpleDateFormat("HH:mm:ss");
		System.out.println(df.format(new java.util.Date())
			+ " Inside incrementCount.");
		boolean locked = lock.tryLock();

		if (locked) {
			System.out.println(df.format(new java.util.Date())
				+ " Lock acquired and counter incremented.");
			counter++;
			lock.unlock();
		} else {
			System.out.println(df.format(new java.util.Date()) +
				" Unable to acquire lock, " +
				"therefore ending processing.");
		}

	}

	public void incrementCount(long timeout) {
		try {
			DateFormat df = new SimpleDateFormat("HH:mm:ss");
			boolean locked = lock.tryLock(timeout, TimeUnit.SECONDS);
			if (locked) {
				System.out.println(df.format(new java.util.Date()) +
					" Lock acquired and counter incremented.");
				counter++;
				lock.unlock();
			} else {
				System.out.println(df.format(new java.util.Date()) +
					" Unable to acquire lock, " +
					"therefore ending processing.");
			}

		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

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

}

The LockData class continues to be a singleton per JVM with some additional refinements. A private int variable counter represents our counter. The incrementCount method’s two overloaded versions are provided for changing the counter variable. Concurrent access control is achieved via the lock variable. The variable is instantiated by a ReentrantLock. The Lock interface provides overloaded tryLock methods. One method does not have any input arguments. It on invokation, tries to acquire a lock and returns back the lock status as response. The other overloaded version tries to acquire a lock for a given duration of time and responses back with the lock status. This is an extremely useful feature of the Lock API. It allows the developer to examine if a specific lock is taken, also allowing the developer to try and acquire the lock for a specific duration of time; in case the lock is unavailable take corrective/compensatory action. This behavior is unlike the synchronized behavior where the application will continue to wait indefinitely until the lock is acquired. One more point to note here is that the developer is responsible for removing the lock explicitly via the unlock method, unlike synchronized where once the execution progresses beyond the synchronized code block or method, the lock is implicitly released.

The concurrent access for LockData was tested using two classes LockThread and LockTest.

public class LockThread implements Runnable {

	private long timeout = 0l;
	private String name = null;

	public LockThread(String name) {
		this.name = name;
	}

	public LockThread(String name, long timeout) {
		this.name = name;
		this.timeout = timeout;
	}

	public void run() {
		LockData data = LockData.getInstance();
		DateFormat df = new SimpleDateFormat("HH:mm:ss");
		System.out.println(df.format(new java.util.Date())
			+" Running thread " + this.name);
		if (this.timeout == 0) {
			data.incrementCount();
		} else {
			data.incrementCount(this.timeout);
		}
		System.out.println(df.format(new java.util.Date()) +
			" Exiting thread " + this.name);
	}

}

public class LockTest {

	public static void main(String[] args) {
		LockThread thd1 = new LockThread("One");
		LockThread thd2 = new LockThread("Two",2l);
		LockThread thd3 = new LockThread("Three");
		LockThread thd4 = new LockThread("Four",3l);

		Thread t1 = new Thread(thd1);
		Thread t2 = new Thread(thd2);
		Thread t3 = new Thread(thd3);
		Thread t4 = new Thread(thd4);

		t1.start();
		t2.start();
		try {
			Thread.sleep(3000);
		} catch (InterruptedException e) {

			e.printStackTrace();
		}
		LockData data = LockData.getInstance();
		data.lockIndefinitely();
		t3.start();
		t4.start();
	}
}
The output generated is:
17:54:44 Running thread One
17:54:44 Running thread Two
17:54:44 Inside incrementCount.
17:54:44 Lock acquired and counter incremented.
17:54:44 Exiting thread One
17:54:44 Lock acquired and counter incremented.
17:54:44 Exiting thread Two
17:54:47 Running thread Four
17:54:47 Running thread Three
17:54:47 Inside incrementCount.
17:54:47 Unable to acquire lock, therefore ending processing.
17:54:47 Exiting thread Three
17:54:50 Unable to acquire lock, therefore ending processing.
17:54:50 Exiting thread Four

In the test I spawned four threads, threads one and three try to acquire locks immediately and increment the counter. Threads 3 and 4 try to acquire the lock for 2 and 3 seconds respectively and try and increment the counter. After initiating threads one and two, post a finite idle time, I have locked the LockData object indefinitely using a utility method lockIndefinitely. The threads three and four are initiated subsequently.

From the output generated you can see that thread one acquires the lock and increments the counter; thread two acquires the lock subsequently and increments the counter. Threads three and four are unable to acquire lock and exit. Thread three exits immediately and thread four exits after trying for 3 seconds. The key point to remember is that unless you invoke the unlock method on the specific LockData object, it remains inaccessible to subsequent thread access. One more thing before we move on, the Lock interface implementation we have used is ReentrantLock. The ReentrantLock allows the current thread to reacquire the lock and proceed with the thread execution but prevents other threads from gaining access. The following test class demonstrates the same:

public class ReentrantLockTest {

	public static void main(String[] args) {

		LockData data = LockData.getInstance();
		data.lockIndefinitely();
		DateFormat df = new SimpleDateFormat("HH:mm:ss");
		System.out.println(df.format(new java.util.Date())
			+ " Initiating counter increment in the" +
			"same thread!!");
		data.incrementCount();
		System.out.println(df.format(new java.util.Date()) +
			" Completing counter increment!");

		LockThread thd = new LockThread("One");
		Thread t = new Thread(thd);
		System.out.println(df.format(new java.util.Date()) +
			" Initiating counter increment in a" +
			"different thread!!");
		t.start();

	}
}
The output is:
18:16:44 Initiating counter increment in thesame thread!!
18:16:44 Inside incrementCount.
18:16:44 Lock acquired and counter incremented.
18:16:44 Completing counter increment!
18:16:44 Initiating counter increment in adifferent thread!!
18:16:44 Running thread One
18:16:44 Inside incrementCount.
18:16:44 Unable to acquire lock, therefore ending processing.
18:16:44 Exiting thread One

I have locked the LockData object indefinitely using the lockIndefinitely method. Post locking, I have successfully invoked the incrementCount method. Therefore as the current thread held the lock, the counter could be incremented. This is reentrancy. To verify subsequently in the code I have spawned a new thread and tried to once again increment the counter. This time it failed, just confirming the reentrant behavior of the lock.

OK so far we have addressed lock contention and lock status awareness. Let’s try and address the fine grained access control requirements of read write access control. Java 5 provides the ReentrantReadWriteLock class to cater to such requirements. The AppData class represents a container class for application specific sharable information. Please find below the source code of AppData:

public class AppData {

	//Contains a lot of application specific data
	private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
	private static AppData data = new AppData();

	private AppData() {

	}

	public static AppData getInstance() {
		return data;
	}

	public ReentrantReadWriteLock getLock() {
		return rwLock;
	}

	public ReentrantReadWriteLock.ReadLock readLock() {
		ReentrantReadWriteLock.ReadLock lock = rwLock.readLock();
		lock.lock();
		return lock;
	}

	public ReentrantReadWriteLock.WriteLock writeLock() {
		ReentrantReadWriteLock.WriteLock lock = rwLock.writeLock();
		lock.lock();
		return lock;
	}
}

Shared access control is achieved via the variable rwLock. End user can select the appropriate locking access level by invoking the readLock or writeLock method. The ReentrantReadWriteLock internally maintains two Lock implementations to represent read access and Write access.

Locks is a utility class to hold read and write lock references during the application lifetime. Here’s the Locks class source code:

public class Locks {

	private static List<ReentrantReadWriteLock.ReadLock> readLocks
		= new ArrayList<ReentrantReadWriteLock.ReadLock>();

	private static List<ReentrantReadWriteLock.WriteLock> writeLocks
		= new ArrayList<ReentrantReadWriteLock.WriteLock>();

	private Locks() {
	}

	public static void addReadLock(ReentrantReadWriteLock.ReadLock lock) {
		readLocks.add(lock);
	}

	public static void addWriteLock(ReentrantReadWriteLock.WriteLock lock) {
		writeLocks.add(lock);
	}

	public static void clearReadLocks() {
		for (Iterator<ReentrantReadWriteLock.ReadLock> locks
			= readLocks.iterator(); locks.hasNext();) {
			ReentrantReadWriteLock.ReadLock lock = locks.next();
			lock.unlock();
		}
	}

	public static void clearWriteLocks() {
		for (Iterator<ReentrantReadWriteLock.WriteLock> locks
			= writeLocks.iterator(); locks.hasNext();) {
			ReentrantReadWriteLock.WriteLock lock = locks.next();
			lock.unlock();
		}
	}
}
Testing the read/write locking functionality is achieved using two classes RWThread and RWTest. Please find below their source:
public class RWThread implements Runnable {

	private boolean writeLocked = false;
	private boolean indefiniteLock = false;

	public RWThread(boolean lockFlag) {
		this.writeLocked = lockFlag;
	}

	public RWThread() {
	}

	public RWThread(boolean lockFlag, boolean indefiniteLock) {
		this.writeLocked = lockFlag;
		this.indefiniteLock = indefiniteLock;
	}

	public void run() {
		AppData data = AppData.getInstance();
		if(this.writeLocked) {
			 ReentrantReadWriteLock.WriteLock lock = data.writeLock();
			 Locks.addWriteLock(lock);
			 System.out.println("Processed Writer Thread!");
			 if (!indefiniteLock) {
				lock.unlock();
			 }
		} else {
			ReentrantReadWriteLock.ReadLock lock  = data.readLock();
			Locks.addReadLock(lock);
			System.out.println("Processed Reader Thread!");
		}
	}
}

public class RWTest {

	public static void main(String[] args) {

		//Create two read locks
		RWThread thd1 = new RWThread();
		RWThread thd2 = new RWThread();

		Thread t1 = new Thread(thd1);
		Thread t2 = new Thread(thd2);

		t1.start();
		System.out.println("Taking the first READ lock");
		inspectLockData();
		t2.start();
		System.out.println("Taking the second READ lock");
		inspectLockData();

		RWThread thd3 = new RWThread(Boolean.TRUE);
		Thread t3 = new Thread(thd3);
		t3.start();
		System.out.println("Taking the first WRITE lock");
		inspectLockData();

		RWThread thd4 = new RWThread(Boolean.TRUE);
		Thread t4 = new Thread(thd4);
		t4.start();
		System.out.println("Taking the second WRITE lock");
		inspectLockData();

		RWThread thd5 = new RWThread();
		Thread t5 = new Thread(thd5);
		t5.start();
		System.out.println("Taking the third READ lock");
		inspectLockData();

		System.out.println("Removing READ locks");
		Locks.clearReadLocks();

		inspectLockData();

		RWThread thd6 = new RWThread(Boolean.TRUE, Boolean.TRUE);
		Thread t6 = new Thread(thd5);
		t6.start();
		System.out.println("Taking the indefinite WRITE lock");
		inspectLockData();

		RWThread thd7 = new RWThread();
		Thread t7 = new Thread(thd5);
		t7.start();
		System.out.println("Taking a READ lock");

		inspectLockData();
	}

	private static void inspectLockData() {

		try {
			//Just to ensure that the thread is processed
			Thread.sleep(2000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}

		AppData data = AppData.getInstance();
		ReentrantReadWriteLock lck = data.getLock();
		System.out.println("Queue length: " + lck.getQueueLength());
		System.out.println("Read Lock: " + lck.getReadLockCount());
		System.out.println("***********");
	}
}

The RWThread class implements the Runnable interface. In its run method, based on the constructor instantiation parameter, the RWThread instance can take either a read or write lock on AppData. If the lock type is read, the lock is retained and not removed unlike a write lock which is taken, an informative line printed on the console and then removed.

The test class RWTest initially spawns two READ lock threads; later invokes two write lock threads and finally spawns a READ lock thread. A utility method inspectLockData is used to display the status of the ReentrantReadWriteLock instance at the pertinent time. The lock’s queue length displays the number of writer threads awaiting access and the read lock count displays the number of threads holding read locks on the lock object.

The output generated is:

Taking the first READ lock
Processed Reader Thread!
Queue length: 0
Read Lock: 1
***********
Taking the second READ lock
Processed Reader Thread!
Queue length: 0
Read Lock: 2
***********
Taking the first WRITE lock
Queue length: 1
Read Lock: 2
***********
Taking the second WRITE lock
Queue length: 2
Read Lock: 2
***********
Taking the third READ lock
Processed Reader Thread!
Queue length: 2
Read Lock: 3
***********
Removing READ locks
Processed Writer Thread!
Processed Writer Thread!
Queue length: 0
Read Lock: 0
***********
Taking the indefinite WRITE lock
Processed Reader Thread!
Queue length: 0
Read Lock: 1
***********
Taking a READ lock
Processed Reader Thread!
Queue length: 0
Read Lock: 2
***********

After the first thread takes a read lock the readlock count is incremented by one. Subsequent initiation of the second thread increases the readlock count to 2. The third and fourth thread try and acquire a WRITE lock. However the lock is unavailable because of the read locks, therefore they are forced into the waiting queue. The queue length is displayed as 2. We programmatically remove the read locks by invoking unlock methods. As soon as the read locks are released the writer threads proceed with their activity and die on completion of processing.

In conclusion, read locks do not wait for each other or writers. A write lock waits for read locks. There is an additional component called Condition class available, however I will not be dwelling on it as it is well explained in the javadoc.

That’s all from me at the moment.

Advertisements

One thought on “java.util.concurrent.locks: Java 5 Concurrency Locks

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s