Read / Write Locks in Java(未完)
A read / write lock is more sophisticated lock than the Lock implementations shown in the text Locks in Java. Imagine you have an application that reads and writes some resource, but writing it is not done as much as reading it is. Two threads reading the same resource does not cause problems for each other, so multiple threads that want to read the resource are granted access at the same time, overlapping. But, if a single thread wants to write to the resource, no other reads nor writes must be in progress at the same time. To solve this problem of allowing multiple readers but only one writer, you will need a read / write lock.
读/写锁比Java锁中的Lock实现更复杂。 假设您有一个读取和写入某些资源的应用程序,但是写比读要少。 读取相同资源的两个线程不会彼此造成问题,因此,读取相同资源的多个线程同时被授予访问权限。 但是,如果单个线程想要写入资源,则不能同时再写或读。 要解决允许多个读但只有一个写的问题,您将需要一个读/写锁。
Java 5在java.util.concurrent包中带有读/写锁定实现。 即使这样,了解其实现背后的理论可能仍然有用。
Java 5 comes with read / write lock implementations in the java.util.concurrent package. Even so, it may still be useful to know the theory behind their implementation.
Read / Write Lock Java Implementation
First let’s summarize the conditions for getting read and write access to the resource:
|
Read Access | If no threads are writing, and no threads have requested write access. |
---|---|
Write Access | If no threads are reading or writing. |
Read Access If no threads are writing, and no threads have requested write access.
Write Access If no threads are reading or writing.
If a thread wants to read the resource, it is okay as long as no threads are writing to it, and no threads have requested write access to the resource. By up-prioritizing write-access requests we assume that write requests are more important than read-requests. Besides, if reads are what happens most often, and we did not up-prioritize writes, starvation could occur. Threads requesting write access would be blocked until all readers had unlocked the ReadWriteLock. If new threads were constantly granted read access the thread (waiting for write access) would remain blocked indefinately, resulting in starvation. Therefore a thread can only be granted read access if no thread has currently locked the ReadWriteLock for writing, or requested it locked for writing.
如果线程想要读取资源,只要没有线程正在写入该资源,并且没有线程请求对该资源进行写,就可以。 通过优先处理写访问请求,我们假设写请求比读请求更重要。 此外,如果读取是最常发生的事情,并且我们没有对写入进行优先排序,则可能会发生starvation直到所有读都解锁了ReadWriteLock为止,请求写访问权限的线程才会被阻塞。 如果不断向新线程授予读取访问权限,则等待写入访问权限的线程将会保持阻塞状态,从而导致starvation。 因此,仅当没有线程写锁定ReadWriteLock或请求写锁定时,才可以授予该线程读访问权限。
A thread that wants write access to the resource can be granted so when no threads are reading nor writing to the resource. It doesn’t matter how many threads have requested write access or in what sequence, unless you want to guarantee fairness between threads requesting write access.
当没有线程正在读/写资源时,可以授权给想要对资源进行写访问的线程,有多少个线程请求写访问权限或请求顺序是无关紧要的,除非您想保证请求写访问权限的线程之间的公平性。
With these simple rules in mind we can implement a ReadWriteLock as shown below:
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();
}
}
The ReadWriteLock has two lock methods and two unlock methods. One lock and unlock method for read access and one lock and unlock for write access.
ReadWriteLock具有两套加锁方法和解锁方法。一套用于读,一套用于写。
The rules for read access are implemented in the lockRead() method. All threads get read access unless there is a thread with write access, or one or more threads have requested write access.
读:lockRead()。若没有写权限或多个请求写,则可以得到读权限。
The rules for write access are implemented in the lockWrite() method. A thread that wants write access starts out by requesting write access (writeRequests++). Then it will check if it can actually get write access. A thread can get write access if there are no threads with read access to the resource, and no threads with write access to the resource. How many threads have requested write access doesn’t matter.
写:lockWrite()。
首先请求写个数+1
然后它将检查是否能得到写权限,若有线程在读或写,则wait;否则写。
It is worth noting that both unlockRead() and unlockWrite() calls notifyAll() rather than notify(). To explain why that is, imagine the following situation:
值得注意的是,unlockRead()和unlockWrite()都调用notifyAll()而不是notify()。为了解释为什么会发生这种情况,请想象以下情况:
Inside the ReadWriteLock there are threads waiting for read access, and threads waiting for write access. If a thread awakened by notify() was a read access thread, it would be put back to waiting because there are threads waiting for write access. However, none of the threads awaiting write access are awakened, so nothing more happens. No threads gain neither read nor write access. By calling noftifyAll() all waiting threads are awakened and check if they can get the desired access.
在ReadWriteLock内部,有多个读和写wait。如果notify()唤醒的线程是读,因为有写在wait,所以此线程继续wait。在这种情况下,所有等待写访问的线程都不会被唤醒,因此读写都没发生,相当于白唤醒。通过调用noftifyAll(),所有正在等待的线程都将被唤醒,会挨个检查是否达到执行条件,从而总会有一个写线程会获得写权限。
Calling notifyAll() also has another advantage. If multiple threads are waiting for read access and none for write access, and unlockWrite() is called, all threads waiting for read access are granted read access at once - not one by one.
调用notifyAll()也有另一个优点。如果有多个线程正在等待读取访问,而没有一个线程正在等待写入访问,并且调用了unlockWrite(),则所有等待读取访问的线程都会被一次授予读取访问权限-而不是一个接一个。
Read / Write Lock Reentrance
The ReadWriteLock class shown earlier is not reentrant. If a thread (that has write access) requests it again, it will block because there is already one writer - itself. Furthermore, consider this case:
前面显示的ReadWriteLock类不是可重入的。 如果具有写访问权的线程再次请求它,则它将阻塞,因为本身已经有一个写程序。 此外,请考虑这种情况:
-
Thread 1 gets read access.
线程1获得读取访问权限。 -
Thread 2 requests write access but is blocked because there is one reader.
线程2请求写访问,但由于有一个读线程而被阻塞。【注意lockReader()执行完就可以解锁ReadWriterLock实例,从而写线程可执行lockWrite()】 -
Thread 1 re-requests read access (re-enters the lock), but is blocked because there is a write request
线程1重新请求读(重新输入锁),但由于存在写请求而被阻塞。
In this situation the previous ReadWriteLock would lock up - a situation similar to deadlock. No threads requesting neither read nor write access would be granted so.
在这种情况下,ReadWriteLock将被锁定-这种情况类似于死锁。 读或写请求都不会被授权。【线程2等待线程1读完成,线程1等待线程2写完成】
To make the ReadWriteLock reentrant it is necessary to make a few changes. Reentrance for readers and writers will be dealt with separately.
要使ReadWriteLock可重入,必须进行一些更改。 对于读和作的重入将单独处理。
Read Reentrance
To make the ReadWriteLock reentrant for readers we will first establish the rules for read reentrance:
- A thread is granted read reentrance if it can get read access (no writers or write requests), or if it already has read access (regardless of write requests).
重入读或者没有(写、请求写)时的新线程,能授予读权限
在可重入读时并没有考虑写请求,if(isReader(callingThread) return true;则直接返回,也就是可重入读优先级>writeRequest
To determine if a thread has read access already a reference to each thread granted read access is kept in a Map along with how many times it has acquired read lock. When determing if read access can be granted this Map will be checked for a reference to the calling thread. Here is how the lockRead() and unlockRead() methods looks after that change:
变量readingThreads的意义:确定线程是否已具有读取访问权限
Thread:已经授予读权限的线程 Integer:重入次数
public class ReadWriteLock{
/*
Thread:已经授予读权限的线程 Integer:重入次数
意义:确定线程是否已具有读取访问权限
*/
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);
//如果为1,则直接从读线程表中删除即可
if(accessCount == 1){ readingThreads.remove(callingThread); }
//如果>1,则说明是可重入读,还不能完全删除,将其重入次数-1
else { readingThreads.put(callingThread, (accessCount -1)); }
notifyAll();
}
//是否可以授予读权限(若其中一个符合if,则return 不会再执行之后的代码)
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();
}
//若为null,说明是新的线程;若不是null,说明是线程重入读
private boolean isReader(Thread callingThread){
return readingThreads.get(callingThread) != null;
}
}
As you can see read reentrance is only granted if no threads are currently writing to the resource. Additionally, if the calling thread already has read access this takes precedence over any writeRequests.
如您所见,读重入仅在当前没有线程写入资源时才被授予。 另外,如果调用线程已经具有读取访问权限,则此优先级高于所有writeRequests。
Write Reentrance
Write reentrance is granted only if the thread has already write access. Here is how the lockWrite() and unlockWrite() methods look after that change:
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++; //写访问个数 因为写可重入,所以值可以>1
writingThread = callingThread;
}
public synchronized void unlockWrite() throws InterruptedException{
writeAccesses--;
//因为一个线程可以多次写,所以==0,说明已经写完
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;
}
}
Notice how the thread currently holding the write lock is now taken into account when determining if the calling thread can get write access.
注意,在确定调用线程是否可以进行写访问时,现在如何考虑当前持有写锁的线程。
Read to Write Reentrance
Sometimes it is necessary for a thread that have read access to also obtain write access. For this to be allowed the thread must be the only reader. To achieve this the writeLock() method should be changed a bit. Here is what it would look like:
有读、写访问权限。线程必须只能为1个读者。对writeLock()做改变:
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;
}
}