Java5 already contains read and write locks in the java.util.concurrent package. Nevertheless, we should understand the principles behind its implementation.
Java implementation of read/write lock
Let's first give an overview of the conditions for reading and writing access to resources:
Reading No thread is doing the write operation, and no thread is requesting the write operation.
No thread is doing read and write operations.
If a thread wants to read a resource, as long as no thread is writing to the resource and no thread requests writing to the resource. We assume that requests for write operations are more important than requests for read operations, so we must increase the priority of write requests. In addition, if read operations occur frequently and we do not increase the priority of write operations, then "hunger" will occur. The thread requesting the write operation will be blocked until all read threads are unlocked from ReadWriteLock. If the read operation permissions of the new thread are always guaranteed, the thread waiting for the write operation will continue to block, and the result will be "hunger". Therefore, the read operation can only be guaranteed to continue when no thread is locking ReadWriteLock for write operations, and no thread requests the lock to be ready for write operations.
When other threads do not read or write operations on the shared resource, a thread may obtain a write lock for the shared resource and then write operations on the shared resource. It doesn't matter how many threads request write locks and in what order, unless you want to ensure fairness of the write lock request.
According to the above description, a read/write lock is simply implemented, and the code is as follows
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(); }}In the ReadWriteLock class, read lock and write lock each have a method to acquire and release the lock.
The implementation of read lock is in lockRead(). As long as no thread has a write lock (writers==0) and no thread is requesting a write lock (writeRequests==0), all threads that want to obtain a read lock can be successfully obtained.
The implementation of write lock is in lockWrite(). When a thread wants to obtain a write lock, it will first add 1 to the write lock request number (writeRequests++), and then determine whether it can really obtain a write lock. When no thread holds a read lock (readers==0) and no thread holds a write lock (writers==0) you can obtain a write lock. It doesn't matter how many threads are requesting to write locks.
It should be noted that in both unlockRead, unlockWrite, the notifyAll method is called instead of notify. To explain this reason, we can imagine the following situation:
If a thread is waiting to acquire the read lock, and a thread is waiting to acquire the write lock. If one of the threads waiting for the read lock is awakened by the notify method, but because there is still a thread requesting the write lock (writeRequests>0), the awakened thread will enter the blocking state again. However, none of the threads waiting for the write lock were awakened, as if nothing had happened (Translator's note: signal loss). If you use the notifyAll method, all threads will be awakened and then determine whether they can obtain the lock they requested.
There is also one benefit to using notifyAll. If multiple read threads are waiting for the read lock and no thread is waiting for the write lock, after calling unlockWrite(), all threads waiting for the read lock can immediately successfully acquire the read lock - instead of only one at a time.
Re-entry of read/write lock
The read/write lock implemented above is not reentrant and will be blocked when a thread that already holds the write lock requests the write lock again. The reason is that there is already a writing thread - it is itself. Also, consider the following example:
In order to make ReadWriteLock reentrable, some improvements need to be made to it. The following will handle the reentry of read lock and the reentry of write lock respectively.
Read lock reenter
In order to make the read lock of ReadWriteLock reentrant, we must first establish rules for the read lock reentrant:
To ensure that the read lock in a thread is reentrant, either meet the conditions for obtaining the read lock (no write or write request) or already hold the read lock (regardless of whether there is a write request or not). To determine whether a thread has already held a read lock, a map can be used to store the thread that has already held a read lock and the number of times the corresponding thread acquires a read lock. When it is necessary to determine whether a thread can obtain a read lock, the data stored in the map is used to make a judgment. The following is the modified code of the methods lockRead and unlockRead:
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; }}In the code, we can see that reentry of read locks is allowed only if no thread has a write lock. In addition, reentrant read locks have higher priority than write locks.
Write lock reenter
Write lock reentry is allowed only if a thread already holds a write lock (regain the write lock). The following is the modified code of the methods lockWrite and unlockWrite.
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; }}Pay attention to how to deal with it when determining whether the current thread can acquire the write lock.
Read lock upgrade to write lock
Sometimes, we want a thread that has a read lock to obtain a write lock. To allow such operations, this thread is required to be the only thread with a read lock. WriteLock() needs to make some changes to achieve this goal:
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; }}Now the ReadWriteLock class can be upgraded from a read lock to a write lock.
Write lock down to read lock
Sometimes threads that have write locks also want to get read locks. If a thread has a write lock, then naturally other threads cannot have a read lock or a write lock. Therefore, there is no danger for a thread that has a write lock and then obtains a read lock. We only need to make a simple modification to the above canGrantReadAccess method:
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; }}Complete implementation of reentrant ReadWriteLock
Below is the complete ReadWriteLock implementation. In order to facilitate the reading and understanding of the code, the above code has been simply refactored. The refactored code is as follows.
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() in finally
When using ReadWriteLock to protect critical zones, if the critical zone may throw an exception, it is important to call readUnlock() and writeUnlock() in the finally block. This is done to ensure that ReadWriteLock can be successfully unlocked, and other threads can request the lock. Here is an example:
lock.lockWrite();try{ //do critical section code, which may throw exception} finally { lock.unlockWrite();}The above code structure can ensure that ReadWriteLock will also be released when an exception is thrown in the critical area. If the unlockWrite method is not called in the finally block, when an exception is thrown in the critical section, ReadWriteLock will remain in the write lock state, which will cause all threads calling lockRead() or lockWrite() to block. The only factor that can re-unlock ReadWriteLock may be that ReadWriteLock is reentrant. When an exception is thrown, the thread can successfully acquire the lock, then execute the critical section and call unlockWrite() again, which will release ReadWriteLock again. But what if the thread no longer acquires the lock? Therefore, calling unlockWrite in finally is very important for writing robust code.
The above is a compilation of Java multi-threaded information. We will continue to add relevant information in the future. Thank you for your support for this website!