Deadly Java concurrent programming (7): ReentrantReadWriteLock source code analysis

In this article, let's take a look at the source code analysis of ReentrantReadWriteLock, based on Java 8.

Reading suggestions: Since the locks in the Java concurrency package are implemented based on AQS, the read-write locks in this article are no exception. If you don't understand it yet, it will be more difficult to read. I recommend that you read the article on the AbstractQueuedSynchronizersource resolution.
Detailed explanation of AQS, this time I will thoroughly understand the principle of locking in Java concurrent packages, so I don’t have to memorize it every interview

What is a read-write lock?

When it comes to locks, you might think of implementations such as the synchronized keyword ReentrantLock, which are exclusive locks. That is, only one thread can access at the same time, and the read-write lock can allow multiple reader threads to access at the same time, but when the writer thread accesses, both the reader thread and the writer thread will be blocked. The read-write lock maintains a pair of locks, one read lock and one write lock. The separation of read-write locks makes concurrency greatly improved compared to exclusive locks.

We can think that the significance of the existence of read-write locks is that in general, the read scene is much larger than the write scene. Therefore, it provides higher concurrency and throughput than exclusive locks in scenarios where reads are greater than writes. The implementation of the read-write lock provided in the Java concurrency package is ReentrantReadWriteLock.

The main features of ReentrantReadWriteLock are listed below. First, get a general understanding, and then analyze the source code in detail.

characteristic Description
Fair choice Supports two lock acquisition methods, fair and unfair (default), with large throughput in unfair mode
Reenter Supports reentry, that is, the reader thread can continue to acquire the read lock after acquiring the read lock, and the write thread can continue to acquire the write lock after acquiring the write lock, and can also acquire the read lock at the same time
Lock downgrade Follow the sequence of acquiring the write lock first, acquiring the read lock, and releasing the write lock to achieve the process of downgrading the write lock to read and write

Read-write lock interface and usage example

ReadWriteLockOnly two methods for acquiring a read lock and a write lock are defined, namely the readLock() method and the writeLock()
method, and its implementation-ReentrantReadWriteLock, in addition to the interface method, also provides some easy to monitor its
internal working status The methods are listed as follows:

Method name Description
getReadLockCount() The number of read locks held, the number of threads that do not hold locks, because a thread can acquire read locks multiple times, here is the total number of read locks acquired
getReadHoldCount() The number of times the current thread holds a read lock, stored in ThradLocal
isWriteLocked() Determine whether the write lock is acquired
getWriteHoldCount() Returns the number of write locks held by the current thread

Usage example

The following example illustrates the use of read-write locks very vividly:

Insert picture description here

In the read operation get(String key) method, the read lock needs to be acquired, which makes concurrent access to the method not blocked. Write operation put (String key, Object value) method and clear () method, you must obtain the write lock in advance when updating the HashMap.

Cache uses read-write locks to improve the concurrency of read operations, and also ensures the visibility of all read and write operations for each write operation, and simplifies the programming method.

ReentrantReadWriteLock overview

Insert picture description here

Take a closer look at the information in the figure above. The read-write locks correspond to two internal nested class instances, and a custom Sync synchronizer inherits AQS. ReadLock and WriteLock share a Sync instance.

Let's take a look at the specific code of ReadLock and WriteLock to understand it more clearly:

Insert picture description here

It is clear that the methods in ReadLock and WriteLock are implemented through the Sync class. Sync is a subclass of AQS, and then derives fair and unfair modes.

From the Sync method they call, we can see: ReadLock uses shared mode , and WriteLock uses exclusive mode .

Here comes the question, the same Sync instance has only one state synchronization state, how can we use shared mode and exclusive mode at the same time ? ? ?

If you can’t understand the above question, then you may be unfamiliar with AQS. Here I briefly list the shared mode and exclusive mode process of AQS. You can compare it horizontally:

Insert picture description here

The essence of AQS's realization of locks lies in the internal property state maintained :

  1. For exclusive acquisition of synchronization status, 0 means that the lock can be acquired, 1 means that it has been robbed by others and cannot be acquired, and the current reentry is possible;
  2. Shared access to the synchronization state, each thread can perform addition and subtraction operations on the state, so the difference from the exclusive type is to ensure the thread-safe operation synchronization state, which is generally guaranteed by loops and CAS.

In other words, the exclusive mode and the shared mode have completely different operations on the state. How is the state used in the read-write lock ReentrantReadWriteLock? Don't worry, continue to look down, this design is quite clever.

Source code analysis of read-write lock

This source code analysis, including read-write state design , write lock acquisition and release , acquire and release read locks and lock downgrade .

1. Read and write state design

As mentioned above, the custom synchronizer of the read-write lock needs to maintain the state of multiple read threads and one write thread in the synchronization state (an integer variable). The answer is "bitwise use". The read-write lock divides the 32-bit state into the upper 16 bits and the lower 16 bits, indicating read and write respectively.

So how does the read-write lock quickly determine the current read and write status? The answer is through bit operations.
Assuming that the current synchronization status value is S, the write status is equal to S&0x0000FFFF (all the upper 16 bits are erased), and the read status is equal to S>>>16 (unsigned 0 is shifted to the right by 16 bits). When the write status increases by 1, it is equal to S+1 , and when the read status increases by 1, it is equal to S+(1<<16) , which is S+0x00010000.

According to the division of the state, an inference can be drawn: When S is not equal to 0, when the write state (S&0x0000FFFF) is equal to 0, the read state (S>>>16) is greater than 0, that is, the read lock has been acquired.

This conclusion is very important and will be reflected in the following code.

With the above foundation, we will not be verbose below, and go directly to the topic and see how it is implemented in the source code. There is not much code. I believe if you understand the above, just look at the code line by line.

2. Acquisition and release of write lock

  • The write lock is an exclusive lock.
  • If a read lock is occupied, the write lock acquisition must enter the blocking queue and wait.

Write lock acquisition

Let's first look at the write lock acquisition method implemented by the custom synchronizer Sync in the ReentrantReadWriteLock read-write lock.

Insert picture description here

Let's take a look at the judgment of writerShouldBlock(), and the code comments are clear at a glance

Insert picture description here

You should have understood the above code. Here is an explanation of why the write lock cannot be acquired if the read lock has been acquired?

It is mainly combined with the original intention of the design and the scene of use. The read-write lock must ensure that the operation of the write lock is visible to the read lock. If the read lock has been acquired and the write lock is still acquired by other threads, the thread that has acquired the read lock cannot perceive it. The operation of acquiring the write lock thread.

Write lock release

Next, we look at the release of the write lock:

Insert picture description here

3. Acquisition and release of read locks

  • The read lock is a shared lock;
  • The read lock can be acquired by multiple threads at the same time. When the write status is 0 (the write lock has not been acquired), the read lock will always be successfully accessed;

Obtaining the source code of the read lock is relatively complicated. From Java 5 to Java 6, it has become a lot more complicated. The main reason is that some new functions have been added, such as the getReadHoldCount() method, which returns the number of times the current thread has acquired the read lock. The read status is the sum of the number of times that all threads acquire a read lock, and the number of times each thread acquires a read lock can only be saved in ThreadLocal and maintained by the thread itself, which complicates the implementation of acquiring a read lock.

So I put it behind. After all, the write lock acquisition is relatively simple, which can greatly enhance the confidence of readers. Next, let's take a look at the realization of this read lock.

Read lock acquisition

The lock process of ReadLock is shown below:

Insert picture description here

The above code is mainly to understand the tryAcquireShared(arg) method:

In AQS, if the return value of the tryAcquireShared(arg) method is less than 0, it means that the shared lock (read lock) is not acquired, and if it is greater than 0, it means that it is acquired.

In the above code, to enter the if branch (that is, to obtain a read lock), you need to meet: readerShouldBlock() returns false, and CAS must succeed (let's not worry about MAX_COUNT overflow).

So based on the above process, we think about how to enter the fullTryAcquireShared(current) method?

  • readerShouldBlock() returns true in 2 cases:

What FairSync says is hasQueuedPredecessors(), that is, there are other elements in the blocking queue waiting for the lock. In other words, in fair mode, someone is queuing, and your newcomer cannot directly acquire the lock;

What is said in NonFairSync is apparentlyFirstQueuedIsExclusive(), that is, to determine whether the first successor node of head in the blocking queue is to acquire the write lock, if so, let the write lock come first to avoid write lock starvation. The author defines a higher priority for the write lock, so if the thread that acquires the write lock is about to acquire the lock, the thread that acquires the read lock should not grab it. If head.next is not to acquire the write lock, you can grab it at will, because it is an unfair mode, everyone is faster than CAS;

  • compareAndSetState(c, c + SHARED_UNIT) Here CAS fails and there is competition. It may compete with another read lock acquisition, and of course it may compete with another write lock acquisition operation.

Then it will come to fullTryAcquireShared and try again:

Insert picture description here

The above source code analysis should be very detailed. If you can't understand the comments in some places above, here I will summarize for you, remove firstReader, cachedHoldCounter, which are used to cache the first read lock acquisition The thread and the last thread that acquired the read lock are essentially used to improve performance. The principle is probably this: usually the acquisition of a read lock is quickly accompanied by a release . Obviously, in the acquisition -> release of the read lock During this time, if no other thread acquires the read lock, this cache can help improve performance, because then there is no need to query the map in ThreadLocal.

Summarize the core process:

Insert picture description here

Read lock release

Let's look at the read lock release process:

Insert picture description here

The process of reading lock release is relatively simple. The main thing is to subtract 1 from the number of read locks held by the current thread. If it is reduced to 0, the corresponding HoldCounter must be removed from ThreadLocal.

Then, in the for loop, the high 16 bits of the state are subtracted by 1. If it is found that both the read lock and the write lock are released, the subsequent thread that acquires the write lock is awakened.

Lock downgrade

Lock degradation refers to the degradation of write locks to read locks . If the current thread has a write lock, then releases it, and finally acquires a read lock, this process of segmented completion cannot be called lock degradation. Lock degradation refers to the process of holding the (currently owned) write lock, acquiring the read lock, and then releasing the (previously owned) write lock.

Let's look at an example of lock degradation:

Insert picture description here

In the above example, when the data is changed, the update variable (boolean and volatile modified) is set to false. At this time, all threads accessing the processData() method can perceive the change, but only one thread can acquire the write lock. Other threads will be blocked on the lock() method of read lock and write lock. After the current thread acquires the write lock and completes data preparation, it acquires the read lock, and then releases the write lock to complete the lock downgrade.

Is it necessary to acquire a read lock in lock degradation?
The answer is necessary, in order to ensure data visibility, because if the current thread does not acquire the read lock and directly releases the write lock, then if another thread acquires the write lock and modifies the data, then the current thread cannot perceive the acquisition of write The changes made by the lock thread.

to sum up

  1. The read-write lock defines a read lock and a write lock, which can hold the write lock and the read lock at the same time, but not vice versa;
  2. When the read lock is held, acquiring the write lock will inevitably fail and enter the blocking queue. You can view the source code tryAcquire(int acquires) of the write lock acquisition to deepen your understanding;
  3. When acquiring a read lock, if the write lock has been acquired but the thread that acquired the write lock is the current thread, then the read lock can still be acquired. Here is also just to understand the steps of lock degradation;
  4. The source code analysis of read-write locks, the acquisition of read locks is difficult to understand, mainly because jdk1.6 introduces functions such as the number of times the current thread lock is acquired, and the read status of each thread can only be saved in ThradLocal, which is maintained by the thread itself At the same time, considering that the probability of acquiring lock conflicts in most cases is small, the firstReader, cachedHoldCounter, etc. are introduced to cache the first and last read lock threads and reentrant times. When viewing the source code, my comments should be very detailed, and it should be understandable to view with this mindset.

(End of the full text) fighting!

Personal public account

Insert picture description here

  • Friends who feel that they are writing well can bother to like and follow ;
  • If the article is incorrect, please point it out, thank you very much for reading;
  • I recommend everyone to pay attention to my official account, and I will regularly push original dry goods articles for you, and pull you into the high-quality learning community;
  • github address: github.com/coderluojust/qige_blogs

Guess you like

Origin blog.csdn.net/taurus_7c/article/details/105891774