Java Concurrency: Read / Write Locks
Jakob has done a great series on Java Concurrency - check out the first 14 articles at his blog. Going forward, we're delighted to announce that you'll also be able to follow the series here on JavaLobby.
A read / write lock is more sophisticated lock than the Lock implementations shown in the text Locks in Java. Imagine you have an application that reads and writes some resource, but writing it is not done as much as reading it is. Two threads reading the same resource does not cause problems for each other, so multiple threads that want to read the resource are granted access at the same time, overlapping. But, if a single thread wants to write to the resource, no other reads nor writes must be in progress at the same time. To solve this problem of allowing multiple readers but only one writer, you will need a read / write lock.
Java 5 comes with read / write lock implementations in the java.util.concurrent package. Even so, it may still be useful to know the theory behind their implementation.
Here is a list of the topics covered in this text:
- Read / Write Lock Java Implementation
- Read / Write Lock Reentrance
- Read Reentrance
- Write Reentrance
- Read to Write Reentrance
- Write to Read Reentrance
- Fully Reentrant Java Implementation
- Calling unlock() from a finally-clause
Read / Write Lock Java Implementation
First let's summarize the conditions for getting read and write access to the resource:
| Read Access | If no threads are writing, and no threads have requested write access. |
| Write Access | If no threads are reading or writing. |
If a thread wants to read the resource, it is okay as long as no threads are writing to it, and no threads have requested write access to the resource. By up-prioritizing write-access requests we assume that write requests are more important than read-requests. Besides, if reads are what happens most often, and we did not up-prioritize writes, starvation could occur. Threads requesting write access would be blocked until all readers had unlocked the ReadWriteLock. If new threads were constantly granted read access the thread waiting for write access would remain blocked indefinately, resulting in starvation. Therefore a thread can only be granted read access if no thread has currently locked the ReadWriteLock for writing, or requested it locked for writing.
A thread that wants write access to the resource can be granted so when no threads are reading nor writing to the resource. It doesn't matter how many threads have requested write access or in what sequence, unless you want to guarantee fairness between threads requesting write access.
With these simple rules in mind we can implement a ReadWriteLock as shown below:
public class ReadWriteLock{
private int readers = 0;
private int writers = 0;
private int writeRequests = 0;
public synchronized void lockRead() throws InterruptedException{
while(writers > 0 || writeRequests > 0){
wait();
}
readers++;
}
public synchronized void unlockRead(){
readers--;
notifyAll();
}
public synchronized void lockWrite() throws InterruptedException{
writeRequests++;
while(readers > 0 || writers > 0){
wait();
}
writeRequests--;
writers++;
}
public synchronized void unlockWrite() throws InterruptedException{
writers--;
notifyAll();
}
}
The ReadWriteLock has two lock methods and two unlock methods. One lock and unlock method for read access and one lock and unlock for write access.
The rules for read access are implemented in the lockRead() method. All threads get read access unless there is a thread with write access, or one or more threads have requested write access.
The rules for write access are implemented in the lockWrite() method. A thread that wants write access starts out by requesting write access (writeRequests++). Then it will check if it can actually get write access. A thread can get write access if there are no threads with read access to the resource, and no threads with write access to the resource. How many threads have requested write access doesn't matter.
It is worth noting that both unlockRead() and unlockWrite() calls notifyAll() rather than notify(). To explain why that is, imagine the following situation:
Inside the ReadWriteLock there are threads waiting for read access, and threads waiting for write access. If a thread awakened by notify() was a read access thread, it would be put back to waiting because there are threads waiting for write access. However, none of the threads awaiting write access are awakened, so nothing more happens. No threads gain neither read nor write access. By calling noftifyAll() all waiting threads are awakened and check if they can get the desired access.
Calling notifyAll() also has another advantage. If multiple threads are waiting for read access and none for write access, and unlockWrite() is called, all threads waiting for read access are granted read access at once - not one by one.
Read / Write Lock Reentrance
The ReadWriteLock class shown earlier is not reentrant. If a thread that has write access requests it again, it will block because there is already one writer - itself. Furthermore, consider this case:
- Thread 1 gets read access.
- Thread 2 requests write access but is blocked because there is one reader.
- Thread 1 re-requests read access (re-enters the lock), but is blocked because there is a write request
In this situation the previous ReadWriteLock would lock up - a situation similar to deadlock. No threads requesting neither read nor write access would be granted so.
To make the ReadWriteLock reentrant it is necessary to make a few changes. Reentrance for readers and writers will be dealt with separately.
(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)





Comments
Jakob Jenkov replied on Wed, 2008/06/11 - 4:31pm
in response to:
David Karr
Hmm... seems like it... I'll have to check what Holub wrote when I get near the book again.
Anyways, this just goes to show how many details there are to get 100% straight in Java Concurrency. That is why I am writing the trail - as notes for myself and fellow developers. I have worked a reasonable amount with Java concurrency, but I am no where near the expertise of Doug Lea, Allen Holub, Brian Goetz etc. Any comments like this article has received are appreciated, and the texts in the tutorial will be updated to reflect any corrections pointed out.
Slava Imeshev replied on Wed, 2008/06/11 - 5:01pm
in response to:
Jakob Jenkov
[quote=jj83777]
Any comments like this article has received are appreciated, and the texts in the tutorial will be updated to reflect any corrections pointed out.
[/quote]
Yes, it does demonstrate that multithreading is hard to do right.
As a friendly suggestion, I'd put a banner or a comment in the code stating that this code is to demonstrate approaches and should not be considered production quality. People tend to consider any cut'n'paste code that complies a production quiality and this one is not. Right now it does not keep track of nested reads deeper than one which will cause a self-deadlock.
Slava
Jakob Jenkov replied on Wed, 2008/06/11 - 5:08pm
Hi Slava, I am working on the correction as we speek which will correct the nested read and write lock errors. I'll post a comment when its ready for you to look at.
David Karr replied on Wed, 2008/06/11 - 5:37pm
in response to:
Slava Imeshev
So Slava, if an expert like you misses the fact that the nested read problem has already been noted and discussed (see the earlier comments), what hope do we have that beginners will read a boilerplate statement about "production quality" and take it to heart? ;)
Slava Imeshev replied on Wed, 2008/06/11 - 5:57pm
in response to:
David Karr
David,
You are right. The priority of spotting this problem belongs to you, indeed :)
I assume double-quoting the production quality bears certain irony which I fail to recognize, just as the references to beginners and experts :) To make sure we are on the same page, that's what I meant: http://tutor2u.net/business/gcse/production_quality_introduction.htm
To get practical and given that the promised fix is still not there, what would be your suggestions regarding addressing the nested read problem?
Slava
Cacheonix - Clustered Cache and Data Grid for Java
Jakob Jenkov replied on Wed, 2008/06/11 - 6:02pm
Impatience is a virtue ;-)
The updates are ready now, here on JavaLobby. I'll repost them on my own blog either later today or tomorrow.
Slava Imeshev replied on Wed, 2008/06/11 - 6:17pm
in response to:
Jakob Jenkov
The updates are ready now, here on JavaLobby. I'll repost them on my own blog either later today or tomorrow.
[/quote]
Awesome! Do you think you could get rid of put access here?
else { readingThreads.put(callingThread, (accessCount -1)); }Slava
Jakob Jenkov replied on Thu, 2008/06/12 - 12:19am
It is probably possible to optimize the design for performance if you want to. For instance, if the read access counter was a class rather than just an int, you could just update the counter. For instance:
public class AccessCount{public int count = 0;
}
Then in the lockRead() and unlockRead() you could do something like this:
Slava Imeshev replied on Thu, 2008/06/12 - 2:59am
Exactly! Great job, Jacob!
Slava