This section is mainly about the implementation of read-write locks.
As mentioned in the previous section, Readwritelock appears to have two locks: readlock/writelock. If it's really two locks, how do they affect each other?
In fact, the realization of lock in Reentrantreadwritelock is done by Java.util.concurrent.locks.ReentrantReadWriteLock.Sync. This class looks familiar, in fact it is a subclass of Aqs, a similar structure that exists in Countdownlatch, Reentrantlock, and semaphore. It also has two implementations: Fair lock and non-fair lock, That's Java.util.concurrent.locks.ReentrantReadWriteLock.FairSync and java.util.concurrent.locks.ReentrantReadWriteLock.NonfairSy. nc Not to mention here.
The lock body inside the reentrantreadwritelock is a sync, which is the Fairsync or nonfairsync mentioned above, so there is actually only one lock, but not the same way to get the read and write locks, So a read-write lock is the only two different views that are exclusive locks.
There are two classes in Reentrantreadwritelock: Readlock/writelock, these two classes are the implementation of lock.
Listing 1 Readlock Fragment
public static class Readlock implements Lock, java.io.Serializable {
Private final sync sync;
Protected Readlock (Reentrantreadwritelock Lock) {
sync = lock.sync;
}
public void Lock () {
Sync.acquireshared (1);
}
public void lockinterruptibly () throws Interruptedexception {
sync.acquiresharedinterruptibly (1);
}
public Boolean trylock () {
return Sync.tryreadlock ();
}
public boolean Trylock (long timeout, timeunit unit) throws Interruptedexception {
Return Sync.tryacquiresharednanos (1, Unit.tonanos (timeout));
}
public void unlock () {
Sync.releaseshared (1);
}
Public Condition newcondition () {
throw new Unsupportedoperationexception ();
}
}
Listing 2 Writelock Fragment
public static class Writelock implements Lock, java.io.Serializable {
Private final sync sync;
Protected Writelock (Reentrantreadwritelock Lock) {
sync = lock.sync;
}
public void Lock () {
Sync.acquire (1);
}
public void lockinterruptibly () throws Interruptedexception {
sync.acquireinterruptibly (1);
}
public Boolean trylock () {
return Sync.trywritelock ();
}
public boolean Trylock (long timeout, timeunit unit) throws Interruptedexception {
Return Sync.tryacquirenanos (1, Unit.tonanos (timeout));
}
public void unlock () {
Sync.release (1);
}
Public Condition newcondition () {
return Sync.newcondition ();
}
public Boolean isheldbycurrentthread () {
return sync.isheldexclusively ();
}
public int Getholdcount () {
return Sync.getwriteholdcount ();
}
}
Listing 1 describes the implementation of a read lock, and listing 2 describes the implementation of a write lock. Obviously Writelock is an exclusive lock, which is almost identical to the implementation in Reentrantlock, all using the Aqs acquire/release operation. Of course, there is a little difference in the internal processing style with Reentrantlock. As you can see in Listing 1 and Listing 2, Readlock acquires a shared lock, and Writelock acquires an exclusive lock.
In the Aqs section, a state field (type int, 32 bits) is described in Aqs to describe how many threads have been held in a lock. In the era of exclusive locks this value is usually 0 or 1 (if re-entry is the number of re-entry), in the age of shared locks is the number of locks held. As mentioned in the previous section, Readwritelock's read and write locks are related but inconsistent, so it takes two numbers to describe the number of read locks (shared locks) and write locks (exclusive locks). Obviously, a state is not enough now. In Reentrantreadwriltelock, the field is divided into two, the high 16 bits represent the number of shared locks, and the low 16 bits represent the number of exclusive locks (or the number of reentrant). 2^16-1=65536, this is why the maximum number of shared and exclusive locks is only 65535 for the last section.
With the above knowledge, it is much easier to analyze the acquisition and release of Read-write locks.
Listing 3 Write lock fetch fragment
Protected Final Boolean tryacquire (int acquires) {
Thread current = Thread.CurrentThread ();
int c = getState ();
int w = Exclusivecount (c);
if (c! = 0) {
if (w = = 0 | | current! = Getexclusiveow Nerthread ())
return false;
if (w + exclusivecount (acquires) > Max_count)
throw new Error ("Maximum lock count Exceeded");
}
if (w = = 0 && writershouldblock (current)) | |
!compareandsetstate (c, C + acquires))
return false;
Setexclusiveownerthread (current);
return true;
}
Listing 3 is the logical fragment of the write lock acquisition, which is the entire workflow:
- The number of lock threads held is not 0 (c=getstate () is not 0), if the number of write threads (W) is 0 (then the number of read threads is not 0) or the exclusive lock thread (the thread holding the lock) is not the current thread to return the failure. or the number of write locks (in fact, the number of reentrant) is greater than 65535 throws an error exception. Otherwise proceed to 2.
- If the write thread is 0 (then the read thread should also be 0 because step 1 already handles the c!=0) and the current thread needs to block then the return fails, and if the increase in the number of write threads fails, the return fails. Otherwise proceed to 3.
- Sets the exclusive thread (the write thread) to be the current thread, which returns TRUE.
Exclusivecount (c) in Listing 3 is the number of write threads (including the number of reentrant), which is the low 16-bit value for state. Another logic here is whether the current write thread needs to block Writershouldblock. Listing 4 and listing 5 are the fragments that need to be blocked in both fair and unfair locks. It is clear that the current thread is always not blocked for an unfair lock, and for a fair lock, if the Aqs queue is not empty or if the thread is not in the queue header of the Aqs then the threads are blocked until the thread in front of the queue has finished processing the lock logic.
Listing 4 Fair read and write lock thread blocking
Final Boolean writershouldblock (Thread current) {
return!isfirst (current);
}
Listing 5 non-fair read/write lock thread blocking
Final Boolean writershouldblock (Thread current) {
return false;
}
It is easier to release a lock when the acquisition logic of the write lock is clear. The write lock release logic fragment described in Listing 6 is actually the number of write locks left to detect, and if 0 will monopolize the lock line Cheng (meaning no thread acquires the lock), otherwise it is currently a release of the re-entry lock, so the exclusive lock line cannot be Cheng empty. Then write back the number of remaining thread states to Aqs.
Listing 6 write lock release logic fragment
Protected Final Boolean tryrelease (int releases) {
int NEXTC = GetState ()-releases;
if (Thread.CurrentThread ()! = Getexclusiveownerthread ())
throw new Illegalmonitorstateexception ();
if (Exclusivecount (nextc) = = 0) {
Setexclusiveownerthread (NULL);
SetState (NEXTC);
return true;
} else {
SetState (NEXTC);
return false;
}
}
The get release process for the write lock described in Listing 3~6. The fetch and release of the read lock is slightly more complicated. Listing 7 describes the fetch process for the read lock.
Listing 7 reading the lock capture process fragment
Protected final int tryacquireshared (int unused) {
Thread current = Thread.CurrentThread ();
int c = GetState ();
if (Exclusivecount (c)! = 0 &&
Getexclusiveownerthread ()! = current)
return-1;
if (Sharedcount (c) = = Max_count)
throw new Error ("Maximum lock count Exceeded");
if (!readershouldblock (current) &&
Compareandsetstate (c, C + shared_unit)) {
Holdcounter RH = Cachedholdcounter;
if (RH = = NULL | | Rh.tid! = current.getid ())
Cachedholdcounter = RH = Readholds.get ();
rh.count++;
return 1;
}
return fulltryacquireshared (current);
}
Final int fulltryacquireshared (Thread current) {
Holdcounter RH = Cachedholdcounter;
if (RH = = NULL | | Rh.tid! = current.getid ())
RH = Readholds.get ();
for (;;) {
int c = GetState ();
int w = Exclusivecount (c);
if (w! = 0 && getexclusiveownerthread ()! = current) | |
((Rh.count | w) = = 0 && readershouldblock (current)))
return-1;
if (Sharedcount (c) = = Max_count)
throw new Error ("Maximum lock count Exceeded");
if (Compareandsetstate (c, C + shared_unit)) {
Cachedholdcounter = RH; Cache for release
rh.count++;
return 1;
}
}
}
The process for reading the lock acquisition is this:
- If the write thread holds a lock (that is, the number of exclusive locks is not 0) and the exclusive thread is not the current thread, then the failure is returned. A read lock is obtained while the write thread is allowed to acquire the lock. Otherwise proceed to 2.
- If the number of Read thread request locks reaches 65535 (including the re-entry lock), then run out with a wrong error, otherwise 3.
- If the read thread does not wait (in fact, if a fair lock is required) and the number of read lock states is increased successfully, the return succeeds, otherwise 4.
- The reason for the failure of Step 3 is that the CAS operation has failed to modify the state number, so it is necessary for the loop to constantly try to modify the state until it succeeds or the lock is written to the thread. It is actually a continuous attempt by process 3 until the CAS count succeeds or is written to a thread-occupied lock.
In Listing 7 There is an object holdcounter, for the moment it is not mentioned what the structure is and why there is such a structure.
Next, let's look at listing 8 to see how to release a read lock. Also ignore Holdcounter, the key is in the for loop inside, is actually a continuously tried CAS operation, until the modification state is successful. The number of shared locks (read locks) described by the high 16 bits of state is mentioned earlier, so it is necessary to subtract 2^16 each time, which is equivalent to the number of read locks minus 1. Actually shared_unit=1<<16.
Listing 8 reading the lock release process
Protected Final Boolean tryreleaseshared (int unused) {
holdcounter RH = Cachedhold Counter;
Thread current = Thread.CurrentThread ();
if (RH = = NULL | | Rh.tid! = current.getid ())
RH = RE Adholds.get ();
if (rh.trydecrement () <= 0)
throw new Illegalmonitorstateexception ();
for (;;) {
int c = getState ();
int nextc = C-shared_unit;
if (Compareandsetstate (c, NEXTC))
return NEXTC = = 0;
}
}
All right, now look back and see what Holdcounter is. First we can see that only add 1 when acquiring a shared lock (read lock), and only subtract 1 when releasing the shared lock, and throws a Illegalmonitorstateexception exception when the lock is released. And we know that illegalmonitorstateexception typically describes a thrown exception that a thread operates on a monitor object that does not belong to itself. This means that a thread releases a shared lock that does not belong to itself or does not exist.
The previous chapters have repeatedly emphasized that for shared locks, it is not the concept of locks, more like the concept of a counter. A shared lock is relative to a counter operation, one acquisition of a shared lock is equivalent to a counter plus 1, releasing a shared lock is equivalent to a counter minus 1. Obviously only a thread holds a shared lock (that is, the current thread carries a counter that describes how many shared or multiple shared locks it holds) before releasing a shared lock. Otherwise, a thread that does not acquire a shared lock invokes a release operation that causes the read-write lock state (the number of threads holding the lock, including the number of reentrant) errors.
When we understand the role of holdcounter, we can guess that it is actually the number of shared locks (read locks) that the current thread holds, including the number of reentrant. Then this number must be tied to the thread.
In Java, an object and thread are bound together, only threadlocal can be implemented. So there's no question that holdcounter should be a counter bound to a thread.
Listing 9 Threads holding the number of read locks counter
Static Final class Holdcounter {
int count;
final Long tid = Thread.CurrentThread (). GetId ();
int trydecrement () {
int c = count;
if (C > 0)
count = c-1;
return C;
}
}
Static final class Threadlocalholdcounter
extends threadlocal public Holdcounter initialvalue () {
return new Holdcounter ();
}
}
Listing 9 describes a counter in which the thread holds the number of read locks. You can see here that the Holdcounter is bound to the current thread using threadlocal, and Holdcounter also holds the thread ID. This allows you to release the lock to know if the last read thread (cachedholdcounter) cached in the Readwritelock is the current thread. The benefit of this is that the number of threadlocal.get () can be reduced because it is also a time-consuming operation. It is important to note that the reason for holdcounter binding thread IDs without binding thread objects is to avoid holdcounter and threadlocal binding and GC is difficult to free them (although GC can intelligently discover such references and reclaim them, but this requires a certain cost), So in fact this is just to help the GC quickly reclaim objects.
In addition to Readlock () and Writelock (), the lock object also allows Trylock (), so Readlock and Writelock () Trylock () are not the same. Listing 10 and listing 11 describe the Trylock () of the read lock and the Trylock () of the write lock, respectively.
The read lock Trylock () is the Tryreadlock () Success Condition: There is no write lock or the write lock is the current thread, and the number of read thread share locks is no more than 65,535.
The Write lock Trylock () is the Trywritelock () Success Condition: No write lock or write lock is the current thread, and attempts to modify state successfully.
Listing 10 reading the lock's Trylock ()
Final Boolean tryreadlock () {
Thread current = Thread.CurrentThread ();
for (;;) {
int c = GetState ();
if (Exclusivecount (c)! = 0 &&
Getexclusiveownerthread ()! = current)
return false;
if (Sharedcount (c) = = Max_count)
throw new Error ("Maximum lock count Exceeded");
if (Compareandsetstate (c, C + shared_unit)) {
Holdcounter RH = Cachedholdcounter;
if (RH = = NULL | | Rh.tid! = current.getid ())
Cachedholdcounter = RH = Readholds.get ();
rh.count++;
return true;
}
}
}
Listing 11 Writing the lock Trylock ()
Final Boolean trywritelock () {
Thread current = Thread.CurrentThread ();
int c = GetState ();
if (c! = 0) {
int w = Exclusivecount (c);
if (w = = 0 | | Current! = Getexclusiveownerthread ())
return false;
if (w = = Max_count)
throw new Error ("Maximum lock count Exceeded");
}
if (!compareandsetstate (c, C + 1))
return false;
Setexclusiveownerthread (current);
return true;
}
The logic of the whole read-write lock is probably so much, in fact, the real research is not very complex, the really complex things are in Aqs inside.
Lock part of the principle and thought are introduced, the next section will be on the Lock Machine section, and the thread concurrency there will be some simple subsections.
In layman's Java Concurrency (14): Lock mechanism Part 9 read-write Lock (Reentrantreadwritelock) (2) [Turn]