2020 java Concurrency and Multithreading Tutorial: Read/Write Locks in java (Re-lockable, ReadWriteLock Completely Re-entrant)

Keywords: Java

Translation: GentlemanTsao, 2020-06-06

Article Directory


Read/write locks in Java are more complex than the Lock implementation example in the Java Lock article.Imagine that you have an application that can read and write some resources, but write resources are not as many as read resources.Two threads reading the same resource do not cause problems with each other, so multiple threads reading the resource can be granted access at the same time, which can overlap.However, if a thread wants to write to a resource, no other reads or writes can be made at the same time.To solve the problem of allowing multiple read threads but only one write thread, you need a read/write lock.

Java 5 inJava.util.concurrentA read/write lock implementation is included with the package.Even so, it is still useful to understand the theory behind its implementation.

Implementation of a java read/write lock

First, let's summarize the conditions for obtaining read and write access to resources:
Read Access
No threads are writing, and no threads are requesting write access.
Write access
No threads are reading or writing.

If a thread wants to read a resource, it can as long as no thread is writing to the resource and no thread requests write access to the resource.We only need to prioritize write access requests, which assume that write requests are more important than read requests.In addition, hunger can occur if the reads are most frequent and we don't increase the write priority.Threads requesting write access will be blocked until all read threads have unlocked ReadWriteLock.If new threads continue to gain read access, threads waiting for write access will remain blocked, causing hunger.Therefore, read access can only be granted to threads if no thread is locking ReadWriteLock for writing or requests to lock ReadWriteLock for writing.

When no threads are reading or writing resources, threads that want permission to write resources can be granted permissions.It doesn't matter how many threads there are and in what order write access is requested, unless you want to ensure fairness between threads requesting write access.

With these simple rules in mind, we can implement the following ReadWriteLock:

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();
  }
}

ReadWriteLock has two locking and two unlocking methods.A lock and unlock method is used for read access, and a lock and unlock method is used for write access.

The rules for read access are implemented in the lockRead() method.All threads have read access unless there is a thread with write access or one or more threads request write access.

Rules for write access are implemented in the lockWrite() method.The thread that wants write access starts by requesting write access (writeRequests ++).It then checks if it really gets write access.Threads can gain write access if no thread has read access to the resource and no thread has write access to the resource.It does not matter how many threads request write access.

It is worth noting that both unlockRead() and unlockWrite() call notifyAll() instead of notify().Why do you want to do this?Consider the following scenarios:

Inside ReadWriteLock, there are threads waiting to read and threads waiting to write.If notify() wakes up a thread that is a read thread, it returns to the wait state because the thread is waiting for write access.However, none of the threads waiting to be written are awakened, so nothing else happens.No threads have read or write access.By calling noftifyAll (), all waiting threads will be awakened and checked to see if they have the required access rights.

There is also an advantage to calling notifyAll().If multiple threads are waiting for read access, and no threads are waiting for write access, and unlockWrite () has been called, all threads waiting for read will be granted read access immediately instead of one by one.

Read/Write Lock Reentrability

The ReadWriteLock class of the previous example is not reentrant.If a thread with write access requests it again, it will block because there is already a write thread -- it itself.In addition, consider this scenario:

  1. Thread 1 has read access.
  2. Thread 2 requested write access but was blocked because there was a read thread.
  3. Thread 1 re-requested read access (re-entered the lock), but was blocked due to write requests.

In this case, the ReadWriteLock above will be locked -- similar to a deadlock.Threads cannot gain permission whether they request to read or write.

To make ReadWriteLock reentrant, some changes must be made.Processes reader and writer thread reentrancy, respectively.

Read Lock Reentry

In order for ReadWriteLock reader threads to be reentrant, we first establish read lock reentrance rules:

  • If a thread can gain read access (no write threads or write requests), or it already has read access (whether there are write requests or not), grant the thread read lock reentrance.

To determine whether a thread already has read access, keep a reference to each thread that has been granted read access in Map and include the number of times it has been locked for read.When determining whether read access can be granted, this Map is checked to obtain a reference to the calling thread.The following are the modified lockRead() and unlockRead() methods:

public class ReadWriteLock{

  private Map<Thread, Integer> readingThreads =
      new HashMap<Thread, Integer>();

  private int writers        = 0;
  private int writeRequests  = 0;

  public synchronized void lockRead() throws InterruptedException{
    Thread callingThread = Thread.currentThread();
    while(! canGrantReadAccess(callingThread)){
      wait();                                                                   
    }

    readingThreads.put(callingThread,
       (getAccessCount(callingThread) + 1));
  }


  public synchronized void unlockRead(){
    Thread callingThread = Thread.currentThread();
    int accessCount = getAccessCount(callingThread);
    if(accessCount == 1){ readingThreads.remove(callingThread); }
    else { readingThreads.put(callingThread, (accessCount -1)); }
    notifyAll();
  }


  private boolean canGrantReadAccess(Thread callingThread){
    if(writers > 0)            return false;
    if(isReader(callingThread) return true;
    if(writeRequests > 0)      return false;
    return true;
  }

  private int getReadAccessCount(Thread callingThread){
    Integer accessCount = readingThreads.get(callingThread);
    if(accessCount == null) return 0;
    return accessCount.intValue();
  }

  private boolean isReader(Thread callingThread){
    return readingThreads.get(callingThread) != null;
  }

}

As you can see, read lock reentrance is only granted when there are currently no threads writing resources.In addition, if the calling thread already has read access, this priority is higher than all write requests.

Write Lock Reentry

Write lock reentrance is granted only if the thread already has write access.The following are the modified lockWrite () and unlockWrite () methods:

public class ReadWriteLock{

    private Map<Thread, Integer> readingThreads =
        new HashMap<Thread, Integer>();

    private int writeAccesses    = 0;
    private int writeRequests    = 0;
    private Thread writingThread = null;

  public synchronized void lockWrite() throws InterruptedException{
    writeRequests++;
    Thread callingThread = Thread.currentThread();
    while(! canGrantWriteAccess(callingThread)){
      wait();
    }
    writeRequests--;
    writeAccesses++;
    writingThread = callingThread;
  }

  public synchronized void unlockWrite() throws InterruptedException{
    writeAccesses--;
    if(writeAccesses == 0){
      writingThread = null;
    }
    notifyAll();
  }

  private boolean canGrantWriteAccess(Thread callingThread){
    if(hasReaders())             return false;
    if(writingThread == null)    return true;
    if(!isWriter(callingThread)) return false;
    return true;
  }

  private boolean hasReaders(){
    return readingThreads.size() > 0;
  }

  private boolean isWriter(Thread callingThread){
    return writingThread == callingThread;
  }
}

Note that when determining if the calling thread can get write access, it is now time to consider the thread currently holding the write lock.

Read Lock to Write Lock Reentry

Sometimes, threads with read access also need write access.To do this, the thread must be the only read thread.To achieve this, slightly change the writeLock() method.Like the following:

public class ReadWriteLock{

    private Map<Thread, Integer> readingThreads =
        new HashMap<Thread, Integer>();

    private int writeAccesses    = 0;
    private int writeRequests    = 0;
    private Thread writingThread = null;

  public synchronized void lockWrite() throws InterruptedException{
    writeRequests++;
    Thread callingThread = Thread.currentThread();
    while(! canGrantWriteAccess(callingThread)){
      wait();
    }
    writeRequests--;
    writeAccesses++;
    writingThread = callingThread;
  }

  public synchronized void unlockWrite() throws InterruptedException{
    writeAccesses--;
    if(writeAccesses == 0){
      writingThread = null;
    }
    notifyAll();
  }

  private boolean canGrantWriteAccess(Thread callingThread){
    if(isOnlyReader(callingThread))    return true;
    if(hasReaders())                   return false;
    if(writingThread == null)          return true;
    if(!isWriter(callingThread))       return false;
    return true;
  }

  private boolean hasReaders(){
    return readingThreads.size() > 0;
  }

  private boolean isWriter(Thread callingThread){
    return writingThread == callingThread;
  }

  private boolean isOnlyReader(Thread thread){
      return readers == 1 && readingThreads.get(callingThread) != null;
      }
  
}

The ReadWriteLock class is now read-to-write access reentrant.

Write lock to read lock reentry

Sometimes threads with write access also need read access.If a write thread requests read access, it should always be granted.If one thread has write access, no other thread can have read or write access, so doing so is not dangerous.The changed canGrantReadAccess () method is as follows:

public class ReadWriteLock{

    private boolean canGrantReadAccess(Thread callingThread){
      if(isWriter(callingThread)) return true;
      if(writingThread != null)   return false;
      if(isReader(callingThread)  return true;
      if(writeRequests > 0)       return false;
      return true;
    }

}

Completely reentrant ReadWriteLock

Below is a fully reentrant ReadWriteLock implementation.I refactored the access conditions to make them easier to read and to convince myself they were correct.

public class ReadWriteLock{

  private Map<Thread, Integer> readingThreads =
       new HashMap<Thread, Integer>();

   private int writeAccesses    = 0;
   private int writeRequests    = 0;
   private Thread writingThread = null;


  public synchronized void lockRead() throws InterruptedException{
    Thread callingThread = Thread.currentThread();
    while(! canGrantReadAccess(callingThread)){
      wait();
    }

    readingThreads.put(callingThread,
     (getReadAccessCount(callingThread) + 1));
  }

  private boolean canGrantReadAccess(Thread callingThread){
    if( isWriter(callingThread) ) return true;
    if( hasWriter()             ) return false;
    if( isReader(callingThread) ) return true;
    if( hasWriteRequests()      ) return false;
    return true;
  }


  public synchronized void unlockRead(){
    Thread callingThread = Thread.currentThread();
    if(!isReader(callingThread)){
      throw new IllegalMonitorStateException("Calling Thread does not" +
        " hold a read lock on this ReadWriteLock");
    }
    int accessCount = getReadAccessCount(callingThread);
    if(accessCount == 1){ readingThreads.remove(callingThread); }
    else { readingThreads.put(callingThread, (accessCount -1)); }
    notifyAll();
  }

  public synchronized void lockWrite() throws InterruptedException{
    writeRequests++;
    Thread callingThread = Thread.currentThread();
    while(! canGrantWriteAccess(callingThread)){
      wait();
    }
    writeRequests--;
    writeAccesses++;
    writingThread = callingThread;
  }

  public synchronized void unlockWrite() throws InterruptedException{
    if(!isWriter(Thread.currentThread()){
      throw new IllegalMonitorStateException("Calling Thread does not" +
        " hold the write lock on this ReadWriteLock");
    }
    writeAccesses--;
    if(writeAccesses == 0){
      writingThread = null;
    }
    notifyAll();
  }

  private boolean canGrantWriteAccess(Thread callingThread){
    if(isOnlyReader(callingThread))    return true;
    if(hasReaders())                   return false;
    if(writingThread == null)          return true;
    if(!isWriter(callingThread))       return false;
    return true;
  }


  private int getReadAccessCount(Thread callingThread){
    Integer accessCount = readingThreads.get(callingThread);
    if(accessCount == null) return 0;
    return accessCount.intValue();
  }


  private boolean hasReaders(){
    return readingThreads.size() > 0;
  }

  private boolean isReader(Thread callingThread){
    return readingThreads.get(callingThread) != null;
  }

  private boolean isOnlyReader(Thread callingThread){
    return readingThreads.size() == 1 &&
           readingThreads.get(callingThread) != null;
  }

  private boolean hasWriter(){
    return writingThread != null;
  }

  private boolean isWriter(Thread callingThread){
    return writingThread == callingThread;
  }

  private boolean hasWriteRequests(){
      return this.writeRequests > 0;
  }

}

Call unlock() from final clause

When using ReadWriteLock to protect a critical zone, the critical zone may throw exceptions, so the readUnlock() and writeUnlock() methods should be called from within the final clause.This ensures that ReadWriteLock can be unlocked so that other threads can lock it.Here is an example:

lock.lockWrite();
try{
  //do critical section code, which may throw exception
} finally {
  lock.unlockWrite();
}

This small structure ensures that ReadWriteLock can be unlocked when an exception is thrown in the code in the critical zone.If unlockWrite() is not called from the final clause and an exception is thrown from the critical zone, ReadWriteLock will always remain write locked, causing all threads calling lockRead() or lockWrite() on the ReadWriteLock instance to pause.The only way to unlock ReadWriteLock again is if ReadWriteLock can be re-entered, the thread that locks the lock when an exception is raised then successfully locks it, executes the critical zone, and then calls unlockWrite () again.That way you can unlock ReadWriteLock again.But why wait for this to happen?What if an exception occurs?Calling unlockWrite() from the final clause is a more reliable solution.

Translation Trivia

Original:
By up-prioritizing write-access requests we assume that write requests are more important than read-requests

Resolution:
"by" to...In this way, the sentence is translated literally as "through".
assume.
However, there are two problems with such translation:
1. Not in accordance with Chinese habits.
2. It does not accurately express the natural cohesion between this sentence and the previous one, that is, "Ask a question" - "Give a plan".
Considering contextual cohesion, another way of expression is used in the translation, as shown below.

Translation:
We only need to prioritize write access requests, which assume that write requests are more important than read requests.

Read more:
Concurrent Series Column: java Concurrency and Multithreading Tutorial 2020 Edition

Posted by cbesh2 on Fri, 05 Jun 2020 18:08:09 -0700