Review Part 1
Now for the releaseReadLock()
method.
synchronized public void releaseReadLock() { readers --; if ((readers == 0) & (writersWaiting > 0)) { notifyAll(); } }
We use the notifyAll()
method as there may be both readers and writers waiting because of more waiting writers than maxWritersWaiting
. In this case, we would like only the writers to get the lock and start writing. So notifyAll()
may wake up a waiting reader, but the reader will quickly get back to waiting state on checking the maxWritersWaiting
variable.
Now the writer lock and unlock.
synchronized public void getWriteLock() { writersWaiting ++; while ( (readers > 0) | writingInProgress) { try { wait(); } catch (InterruptedException ie) { } } writersWaiting -- ; writingInProgress = true; } synchronized public void releaseWriteLock() { writingInProgress = false; notifyAll(); }
When a thread seeking write lock enters getWriteLock()
method, it increments a writersWaiting
variable. If there are any readers or any writer already accessing the resources this thread waits. Note that when the thread releases the write lock by calling releaseWriteLock()
it calls notifyAll()
, the reason is that there could be more than one reader threads waiting and each one of them can then access the resource.
Another notable point here is that because the releaseWriteLock()
method is synchronized, the thread crosses another memory barrier when it exits the method and writes the values in the local memory back to main memory.
Whose Lock Is It Anyway?
If you see the example program RWTest.java, you will see that there is no synchronized block! Also note that after taking a lock (read or write) more than one resource is accessed. What we are doing here is synchronizing on the lock of another object (RWLock in this case). Many texts on Java warn against using such locks of other objects. True, some discipline is required in using locks of other objects; but even if you are synchronizing on the same object you are using, you could inadvertently forget to declare a method synchronized and get some unexpected results. The only constraint in using these read-write locks is that you should only access the resources to be protected aftertaking the suitable lock and once you are done with the execution you should release the lock taken. Let us refer to the Java Language Specification to highlight point further.
Section 17.9 Discussions:
“Any association between locks and variables is purely conventional. . .”
And:
“That a lock may be associated with a particular object or a class is purely a convention. . .”
For thorough understanding of the Java Memory model and thread and memory interaction, you must read the Java Language Specification’s Chapter 17.
Behavior of Example Program
The example program RWTest.java when run on a single CPU machine on the Windows ME platform, indeed, initially has a number of concurrent reader threads. But gradually the access becomes more and more serialized, and there are fewer concurrent reader threads; otherwise, the program behaves very predictably. The reason for this serialized access is the fact that it was run on single CPU and also the way time sliced Windows scheduler is written. However, if the same program is run on a multi CPU Solaris box you will find numerous reader threads at any time.
However, the behavior of RWLock is completely predictable and deterministic on any platform in terms of its primary functionality of assigning many reader lock and one write lock at any point in time.
Finally
For the sake of completeness, we shall clarify some more misunderstanding that is very common.
Atomic operation
It is widely held that operations on primitive variables is atomic. It is true; however, it is mostly not used correctly. Consider the operation i = i + 1
, where i
is a primitive int variable. The steps involved here are:
- Read variable
i
from main memory to threads local memory. - Load variable from local memory to working copy of the thread (most likely registers).
- Load the constant 1.
- Add
i
and 1. - Assign to the variable
i
. - Store
i
to thread’s local copy. - Write back to main memory.
Note that atomicity of primitive variables is only limited to pairing of steps 1 and 2 for reading and pairing of steps 6 and 7 for writing. The value of i
may be corrupted by multiple threads trying to carry out operation i = i + 1
simultaneously. Also note that double and long variables are treated as non-atomic even for the purpose of read, load, store, and write operations, each double and long variable is treated as ‘two’ variables of 32-bits each.
Volatile variables
We talked about memory barrier when the synchronized keyword is encountered. A similar memory barrier is reached when a volatile variable is used. The use of any volatile variable is guaranteed to be preceded by the read from the main memory. Quoting the Java Language Specification “actions on the master copies of volatile variables on behalf of a thread are performed by the main memory in exactly the order the thread requested”. Moreover, the actions on volatile variables are atomic as defined above even for long or double variables.
Conclusion
In this article we have tried to explain the interaction of the shared main memory with the thread’s local memory, the meaning of “synchronization” with respect to this interaction and the mutual exclusion. We have tried to clarify the distinction of an object’s lock and the resources it guards. We have explained the meaning of atomic operations and in explanation of these concepts we have implemented a read-write lock. In order to write good and successful multithreaded code, it is important that we understand how Java performs multithreading and how the Java memory model works.
About the Author
Nasir Khan is a Sun Certified Java programmer, with a B.E. in electrical engineering and a masters degree in systems. His areas of interest include Java applications, EJB, RMI, CORBA, JDBC and JFC. He is presently working with BayPackets Inc. in high-technology areas of telecom software.