| good praise, please, get into the habit of

  • You have one thought, I have one thought, and when we exchange, one person has two thoughts

  • If you can NOT explain it simply, you do NOT understand it well enough

Now the Demo code and technical articles are organized together Github practice selection, convenient for everyone to read and view, this article is also included in this, feel good, please also Star🌟


preface

  • This paper introduces the realization of AQS exclusive acquisition of synchronization state, and takes ReentrantLock as an example to explain how the self-defined synchronizer realizes the mutex lock
  • This paper introduces the implementation of AQS shared synchronization state acquisition, and explains how Semaphore can customize the synchronizer to achieve simple flow limiting

With these two articles in mind, it’s easy to understand ReadWriteLock, which has both exclusive and shared access to synchronized state


ReadWriteLock

ReadWriteLock literally translates to “read/write lock”. In reality, the business scenario of reading too much and writing too little is very common, such as application caching

One thread writes data to the cache, and other threads can directly read the data in the cache, improving the data query efficiency

All the mutex mentioned above are exclusive locks, that is, only one thread is allowed to access at a time. In shareable read scenarios, mutex is obviously inefficient. To improve efficiency, the read-write lock model was created

Efficiency is one thing, but concurrent programming is more about efficiency without accuracy

If a writer thread changes a value in the cache, the other reader threads must be “aware” of it, otherwise the query value may be inaccurate

So the read/write lock model has the following three provisions:

  1. Allows multiple threads to read shared variables simultaneously
  2. Only one thread is allowed to write shared variables
  3. If the writer thread is performing a write operation, the other reader threads are prohibited from reading the shared variable

ReadWriteLock is an interface with only two internal methods:

public interface ReadWriteLock {
    // Return the lock used for reading
    Lock readLock(a);

    // Returns the lock used for writing
 Lock writeLock(a); } Copy the code

So to understand the full application of the read/write lock, you need to start with its implementation class, ReentrantReadWriteLock

ReentrantReadWriteLock class structure

Compare the class structure of ReentrantReadWriteLock and ReentrantLock


ReentrantReadWriteLock: ReentrantReadWriteLock: ReentrantReadWriteLock: ReentrantReadWriteLock: ReentrantReadWriteLock: ReentrantReadWriteLock: ReentrantReadWriteLock


The yellow color of the lock degradation is not visible, here is an impression, will be explained separately below

In addition, if you remember, the Java AQS queue synchronizer and ReentrantLock application say that Lock and AQS synchronizer exist in a combination of forms, since there are two types of Lock read/write, their combination mode is divided into two:

  1. Aggregation of read locks and custom synchronizers
  2. Write lock and custom synchronizer aggregation
    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }
Copy the code

This is just a reminder that the mode has not changed, so don’t be confused by the read/write locks

Basic example

ReentrantReadWriteLock is an example of how to use ReentrantReadWriteLock

public class ReentrantReadWriteLockCache {

 // Define a non-thread-safe HashMap to cache objects
 static Map<String, Object> map = new HashMap<String, Object>();
 // Create a read/write lock object
 static ReadWriteLock readWriteLock = new ReentrantReadWriteLock();  // Build the read lock  static Lock rl = readWriteLock.readLock();  // Build the write lock  static Lock wl = readWriteLock.writeLock();   public static final Object get(String key) {  rl.lock();  try{  return map.get(key);  }finally {  rl.unlock();  }  }   public static final Object put(String key, Object value){  wl.lock();  try{  return map.put(key, value);  }finally {  wl.unlock();  }  } } Copy the code

You see, it’s that simple to use. As you know, the core of AQS is the implementation of the lock, that is, control the synchronization state value, ReentrantReadWriteLock is also used to control the synchronization state, so the problem is:

How can an int state control both read and write synchronization?

Clearly a bit of design is needed

Read and write state design

If you want to maintain multiple states on a variable of type int, you will definitely need to split. We know that ints are 32 bits, so we have an opportunity to use state bitwise. We cut it into two parts:

  1. The high 16 bits indicate read
  2. The lower 16 bits indicate write

ReentrantReadWriteLock Sync is a bit operation for JDK1.8, ReentrantReadWriteLock, and Sync

abstract static class Sync extends AbstractQueuedSynchronizer {
       

        static final int SHARED_SHIFT   = 16;
        static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
 static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;  static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;    static int sharedCount(int c) {  return c >>> SHARED_SHIFT;  }   static int exclusiveCount(int c) {  return c & EXCLUSIVE_MASK;  } } Copy the code

At first glance, it seems a little complicated and scary. Don’t panic, we can solve the whole bit operation process with a few small math problems


The entire read/write state calculation in ReentrantReadWriteLock is using these math problems over and over again, so I hope you understand this simple calculation before reading this

Enough foundation, let’s move on to source code analysis

Source code analysis

Write lock analysis

Since write locks are exclusive, you must override the tryAcquire method in AQS

        protected final boolean tryAcquire(int acquires) {        
            Thread current = Thread.currentThread();
           // Get the overall value of state
            int c = getState();
            // Get the write status value
 int w = exclusiveCount(c);  if(c ! =0) {  // w=0: according to inference 2, the overall state is not equal to zero, the write state is equal to zero, therefore, the read state is greater than 0, namely, there is a read lock  // Or the current thread is not the thread that has acquired the write lock  // If either condition is true, the write status fails to be obtained  if (w == 0|| current ! = getExclusiveOwnerThread()) return false;  if (w + exclusiveCount(acquires) > MAX_COUNT)  throw new Error("Maximum lock count exceeded");  // Update the write status value according to inference # 1  setState(c + acquires);  return true;  }  if (writerShouldBlock() || ! compareAndSetState(c, c + acquires)) return false;  setExclusiveOwnerThread(current);  return true;  } Copy the code

There is nothing mysterious about writerShouldBlock (line 19), it is just a judgment on how to get the lock fairly/unfairly (if there is a precursor node)


You see, the write lock acquisition method is that simple

Read lock analysis

Since the read lock is shared, you must override the tryAcquireShared method in AQS

        protected final int tryAcquireShared(int unused) {
            Thread current = Thread.currentThread();
            int c = getState();
           // Write status is not equal to 0, and the holder of the lock is not the current thread, according to convention 3, then acquire read lock failed
            if(exclusiveCount(c) ! =0 &&
getExclusiveOwnerThread() ! = current) return -1;  // Get the read status value  int r = sharedCount(c);  // This place is a little different  if(! readerShouldBlock() && r < MAX_COUNT &&  compareAndSetState(c, c + SHARED_UNIT)) {  if (r == 0) {  firstReader = current;  firstReaderHoldCount = 1;  } else if (firstReader == current) {  firstReaderHoldCount++;  } else {  HoldCounter rh = cachedHoldCounter;  if (rh == null|| rh.tid ! = getThreadId(current)) cachedHoldCounter = rh = readHolds.get();  else if (rh.count == 0)  readHolds.set(rh);  rh.count++;  }  return 1;  }  // If the read lock fails to be acquired, the spin lock is entered  return fullTryAcquireShared(current);  } Copy the code

ReaderShouldBlock and writerShouldBlock are both fair locks, but writerShouldBlock is not fair locks.

final boolean readerShouldBlock(a) {
 return apparentlyFirstQueuedIsExclusive();
}

final boolean apparentlyFirstQueuedIsExclusive(a) {
 Node h, s;  return(h = head) ! =null &&  // Wait for the next node of the queue head node (s = h.next) ! =null &&  // if it is an exclusive node ! s.isShared() &&s.thread ! =null; } Copy the code

In simple terms, if the request is read lock head nodes of the current thread found synchronous queue the next node is exclusive type node, so that means there is a thread waiting for access to write locks (for a write lock failure, into the synchronous queue), then the request read lock the thread will be blocked, after all, more than read write less, if you don’t have this mechanism, Write locks may occur [hunger]

If all of the above conditions are met, you will enter lines 14 through 25 of the tryAcquireShared code, which counts the number of times the thread has held the lock. Read locks are shared, and to keep track of how many times each thread holds a read lock, we need to use ThreadLocal. Since this does not affect the value of synchronization state, we will leave the relationship there


This is the end of the read lock acquisition, which is a little more complicated than the write lock, so let’s explain the lock upgrade/downgrade problem that might confuse you

Upgrade and degrade read/write locks

Read locks can be shared by multiple threads, while write locks are exclusive to single threads. In other words, write locks have higher concurrency limits than read locks


Before we can really look at read-write lock upgrades and degradations, we need to refine the ReentrantReadWriteLock example at the beginning of this article

 public static final Object get(String key) {
  Object obj = null;
  rl.lock();
  try{
      // Get the value in the cache
 obj = map.get(key);  }finally {  rl.unlock();  }  // The value in the cache is not null and is returned directly  if(obj! =null) {  return obj;  }   // If the value in the cache is null, the database is queried by write lock and written to the cache  wl.lock();  try{  // Try again to get the value in the cache  obj = map.get(key);  // The value in the cache is null again  if (obj == null) {  / / query the DB  obj = getDataFromDB(key); // pseudo-code: getDataFromDB  // Put it in the cache  map.put(key, obj);  }  }finally {  wl.unlock();  }  return obj;  } Copy the code

Some children may have doubts

Inside the write lock, why does line 19 fetch the value from the cache again? Isn’t it superfluous?

It is necessary to try again to get the value in the cache, because there may be multiple threads executing the get method at the same time, and the argument key is the same, up to line 16, wl.lock(), for example:


Thread A, thread B, and thread C simultaneously execute to the critical section wl.lock(). Only thread A succeeds in acquiring the write lock, thread B, and thread C can only block until thread A releases the write lock. When thread B or C enters the critical section again, thread A has already updated the value to the cache, so thread B and C do not need to query DB again, but try to query the value in the cache again

If there is no value in the cache, then I can obtain the write lock again to query the DB, like this:

 public static final Object getLockUpgrade(String key) {
  Object obj = null;
  rl.lock();
  try{
   obj = map.get(key);
 if (obj == null) { wl.lock();  try{  obj = map.get(key);  if (obj == null) {  obj = getDataFromDB(key); // pseudo-code: getDataFromDB  map.put(key, obj);  }  }finally {  wl.unlock();  }  }  }finally {  rl.unlock();  }   return obj;  } Copy the code

This is not possible, because acquiring a write lock requires releasing all the read locks first. If two read locks attempt to acquire the write lock, and neither release the read lock, a deadlock occurs, so upgrading the lock is not allowed here

Read/write lock upgrade is not possible, but lock degradation is possible? This is an example of lock degradation from the Oracle website. I pasted the code here. If you are interested, you can click on the link to see more content

 class CachedData {
   Object data;
   volatile boolean cacheValid;
   final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

 void processCachedData(a) {  rwl.readLock().lock();  if(! cacheValid) { // The read lock must be released before the write lock is acquired, because lock escalation is not allowed  rwl.readLock().unlock();  rwl.writeLock().lock();  try {  // Again, the reason may be that another thread has already updated the cache  if(! cacheValid) { data = ...  cacheValid = true;  }  // Downgrade to read lock before releasing write lock  rwl.readLock().lock();  } finally {  // Release the write lock and hold the read lock  rwl.writeLock().unlock();  }  }   try {  use(data);  } finally {  rwl.readLock().unlock();  }  }  } Copy the code

A volatile cacheValid variable is declared in the code to ensure its visibility.

  1. The read lock is first acquired. If the cache is unavailable, the read lock is released
  2. The write lock is then acquired
  3. Before changing the data, check the value of cacheValid again, and then modify the data to set cacheValid to true
  4. Then acquire the read lock before releasing the write lock
  5. Data in the cache is available, data in the cache is processed, and the read lock is released

This process is a complete lock degradation process, in order to ensure data visibility, so the question is:

Why does the above code acquire the read lock before releasing the write lock?

If thread A does not acquire the read lock but directly releases the write lock after modifying the data in the cache. If another thread B acquires A write lock and modifs the data, thread A will not be aware that the data has been modified, but thread A also applies cache data, so A data error may occur

If the lock degradation process is followed and thread A acquires the read lock before releasing the write lock, thread B will be blocked while acquiring the write lock until thread A completes its data processing and releases the read lock, thus ensuring data visibility


Here comes the question:

Do I have to degrade to use write locks?

If you understand the above question, I believe this question has been answered. If thread A wants to reuse the data after modifying the data after time-consuming operation, it wants to use its own modified data instead of the data modified by other threads. In this case, lock degradation is indeed required. If you just want to end up with the latest data, rather than the data you just modified, it is ok to release the write lock, acquire the read lock, and then use the data

I want to add a few additional misconceptions you may have:

  • Acquiring a write lock after a read lock has been released is not an upgrade of the lock

  • Acquiring a read lock does not degrade a lock if it has already released a write lock

I’m sure you understand how locks are upgraded and degraded, and why they are allowed or prohibited

conclusion

ReentrantReadWriteLock uses state to split bits to achieve read/write synchronization. It also analyzes the process of read/write lock obtaining synchronization status through source code. Finally, it also understands the upgrade/degradation mechanism of read/write lock. By now you have a good understanding of read-write locks. If you are having trouble understanding anything in the article, I strongly encourage you to go back to the first two articles of this article, where a lot of material is laid out. Let’s take a look at CountDownLatch, the last concurrency utility class that applies AQS

Soul asking

  1. Read locks do not modify data and allow shared fetching, so why set read locks?
  2. How do you ensure that cached data is consistent in a distributed environment?
  3. When you open to see ReentrantReadWriteLock source code, you will find that can be used in the WriteLock Condition, but it will throw an UnsupportedOperationException ReadLock use Condition, Why is that?
// WriteLock
public Condition newCondition(a) {
 return sync.newCondition();
}

// ReadLock public Condition newCondition(a) {  throw new UnsupportedOperationException(); } Copy the code

reference

  1. Java Concurrency Combat
  2. The art of Concurrent programming in Java
  3. www.jianshu.com/p/58697bb22…

Personal blog: https://dayarch.top

Add my wechat friends, enter the group entertainment learning exchange, note “enter the group”

Welcome to continue to pay attention to the public account: “One soldier of The Japanese Arch”

  • Cutting-edge Java technology dry goods sharing
  • Efficient tool summary | back “tool”
  • Interview question analysis and solution
  • Technical data to receive reply | “data”

To read detective novel thinking easy fun learning Java technology stack related knowledge, in line with the simplification of complex problems, abstract problems concrete and graphical principles of gradual decomposition of technical problems, technology continues to update, please continue to pay attention to……