Java multithreading-read/write locks and java Multithreading
Java 5 contains the read/write lock in the java. util. concurrent package. Even so, we should understand the principles behind its implementation.
Java Implementation of read/write locks
First, let's give an overview of the conditions for read/write access to resources:
No reading thread is performing write operation, and no thread is requesting write operation.
No write thread is performing read/write operations.
If a thread wants to read a resource, as long as no thread is performing a write operation on the resource and no thread requests a write operation on the resource. We assume that the write request is more important than the read request, and the priority of the write request must be increased. In addition, if read operations occur frequently and we do not increase the write operation priority, then the "Hunger" phenomenon will occur. The thread requesting write operations will be blocked until all read threads are unlocked from ReadWriteLock. If the read operation permission of the new thread is always guaranteed, the thread waiting for the write operation will continue to block, and the result is "hunger ". Therefore, read operations can continue only when no thread is locking ReadWriteLock for write operations and no thread is requesting the lock to prepare for write operations.
When other threads do not perform read or write operations on shared resources, a thread may obtain the write lock for the shared resources and write the shared resources. It doesn't matter how many threads have requested the write lock and the order in which the write lock is requested, unless you want to ensure the fairness of the write lock request.
According to the above description, a read/write lock is implemented. 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, each of the read lock and write lock has a way to obtain and release the lock.
In lockRead (), as long as no thread has a write lock (writers = 0) and no thread has a write lock in the request (writeRequests = 0 ), all threads that want to obtain the read lock can be obtained successfully.
Write lock implementation in lockWrite (). When a thread wants to obtain the write lock, it first adds the number of write lock requests to 1 (writeRequests ++ ), then judge whether the write lock can be obtained. When no thread holds the read lock (readers = 0) and no thread holds the write lock (writers = 0), the write lock can be obtained. The number of threads in the request write lock does not matter.
Note that the policyall method is called in the two lock release methods (unlockRead and unlockWrite), instead of policy. To explain this, we can imagine the following situation:
If a thread is waiting to get the read lock, and a thread is waiting to get the write lock. If one of the threads waiting for the read lock is awakened by the notify method, but there are still threads requesting the write lock (writeRequests> 0) at this time, the Awakened thread will be blocked again. However, the thread waiting for the write lock is not awakened, just as nothing has happened (Translator's note: signal loss ). If the policyall method is used, all threads will be awakened and the request locks can be obtained.
Using policyall has another benefit. If multiple read threads are waiting for the read lock and no threads are waiting for the write lock, after unlockWrite () is called, all threads waiting for the read lock can immediately obtain the read lock-instead of allowing only one read lock at a time.
Re-import of read/write locks
The read/write lock (ReadWriteLock) implemented above cannot be reentrant. When a thread that has the write lock requests the write lock again, it will be blocked. The reason is that there is already a write thread -- it is itself. In addition, consider the following example:
To make ReadWriteLock reentrant, you need to make some improvements to it. Next we will process the re-entry of the read lock and the re-entry of the write lock respectively.
Read lock re-entry
To enable the read locks of ReadWriteLock to be reentrant, we must first create rules for the read locks to be reentrant:
Ensure that the read locks in a thread can be reentrant, either meet the conditions for obtaining the read locks (no write or write requests), or have held the read locks (whether or not there are write requests ). To determine whether a thread has a read lock, you can use a map to store the threads that already hold the read lock and the number of times the corresponding thread obtains the read lock. When you need to determine whether a thread can obtain the read lock, the data stored in map is used for 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 the re-entry of the read lock is allowed only when no thread has a write lock. In addition, the reread lock has a higher priority than the write lock.
Write lock re-entry
Only when a thread has a write lock is allowed to re-import the write lock (re-obtain the write lock ). The following is the modified Code of 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; }}
Note how the write lock can be obtained by the current thread.
Read lock upgrade to write lock
Sometimes we want a thread with a read lock to obtain a write lock. To allow such operations, the thread must be the only thread with a read lock. WriteLock () needs to be modified 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 the read lock to the write lock.
Write lock downgrade to read lock
Sometimes the thread that owns the write lock also wants to get the read lock. If a thread has a write lock, other threads naturally cannot have a read lock or write lock. Therefore, it is not dangerous for a thread with a write lock to obtain a read lock. We just need to make a simple modification to the canGrantReadAccess method above:
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
The complete ReadWriteLock implementation is as follows. In order to facilitate reading and understanding the code, the code above is simply reconstructed. The reconstructed 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 ReadWriteLock is used to protect the critical section, if an exception may be thrown in the critical section, it is very 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 code structure above ensures that ReadWriteLock will be released when an exception is thrown in the critical section. 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 until all lockRead () or lockWrite () calls are performed () the thread is always blocked. The only factor that can re-Unlock ReadWriteLock may be that ReadWriteLock is reentrant. When an exception is thrown, this thread can also successfully obtain the lock, then execute the critical section, and call unlockWrite () again (), this will release ReadWriteLock again. But what if the thread no longer obtains the lock? Therefore, calling unlockWrite in finally is very important for writing robust code.