一个读写锁是一个比在上一篇文章中的Lock实现来说更加复杂的锁。想想下你有一个应用读写一些资源,但是写没有和读一样完成。两个线程读相同的资源不会互相引起问题,以至于想读这个资源的多个线程同时授予访问权限。但是,如果一个线程想写这个资源,那么读和写都必须不能同时在进行中。为了解决这个允许多个读一个写的问题,你需要一个读写锁。
Java 5在java.util.concurrent包中提供了读写锁的实现。即使如此,知道后面实现的原理可能也是有用的。
JAVA实现的读写锁
首先让我们总结下获取对这个资源读写访问权限的场景:
读访问权 如果没有现成正在写,那就没有现成需要写访问权。
写访问权 如果没有线程正在读或者正在写。
如果一个线程想读这个资源,只要没有现成写他是没有问题的,并且没有线程对于这个资源需要这个写的访问权限。通过预先写访问请求,我们假设写请求比读请求更重要。另外,如果读发生的更加频繁,而且我们不优先写,可能饥饿就会发生。请求写访问的线程可能会堵塞直到所有的读请求释放这个ReadWriteLock。如果一个新的线程不断的授予读访问权限,正在等待写访问的线程将会保持无限期的锁定的,就会导致饥饿发生。因此,如果没有线程当前为了写锁住这个ReadWriteLock,那么一个线程可以只是被授予读访问权限,或者请求它锁定为了写。
当没有线程对这个资源正在读或者写的时候,想获取对这个资源有写访问权限的线程可以被授予的。多少个线程已经请求写了或者以什么顺序是没有关系的,除非你能在请求写的线程之间保持公平性。
伴随着这些简单的规则,我们可以像下面这样实现一个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有两个锁的方法和两个释放锁的方法。一个锁和一个释放锁是针对读的,另外一个是针对写的。
对于读访问的这个规则是通过lockRead方法实现的。除非这里有一个写访问的线程,或者更多的线程已经需要写访问,否则所有的读访问的线程都可以访问。
对于写访问的这个规则是通过lockWrite方法来实现的。想获取写访问的一个线程通过请求写访问(writeRequests++)。然后它会去检查是否确定会得到一个写访问。如果这里没有线程对这个资源去读或者没有线程对这个资源去写,那么一个线程就可以去写访问。对于有多少个已经请求写访问的线程是没有关系的。
在unlockRead方法和unlockWrite方法中调用notifyAll方法而不是notify方法是值得注意的。为了解释这个,想想下面的场景:
在ReadWriteLock内部这里有正在等待读访问的线程,以及正在等待写访问的线程。如果被notify方法唤醒的一个线程是一个读访问的一个线程,因为这里有一个正在等待写访问的一个线程以至于它仍然会在远处等待。然而,正在等待写访问的所有线程没有被唤醒的,以至于什么都不会发生。没有线程可以获取读访问或者写访问。通过调用notifyAll方法,所有正在等待的线程将会被唤醒,并且去检查他们是否可以获取到一个理想的访问。
调用notifyAll方法也有一个优势。如果有多个线程正在等待读访问,没有等待写访问的,以及unlockWrite方法被调用了,那么所有等待读访问的线程将会一次性的获取到读访问权限,而不是一个一个的来。
读写重入锁(Read/Write Lock Reentrance)
前面展示的ReadWriteLock类不是可重入的。如果已经有一个写访问的一个线程了,它再次去请求,它将会锁住自己。另外,考虑这样的场景:
- 线程1得到读访问。
- 线程2请求写访问,但是被锁住了,因为有一个读访问的线程。
- 线程1再次请求读访问(重入锁),但是被锁住了,因为这里有一个写请求
在这种场景下,前面的ReadWriteLock将会锁住,跟死锁的场景类似。没有线程可以再获取到读访问或者写访问。
为了使得ReadWriteLock可重入的,它需要做一些改变。多于读和写可重入的需要分别处理。
读可重入(Read Reentrance)
为了使得ReadWriteLock对于读可重入,我们将首先建立这个规则:
- 如果一个线程可以得到读访问(没有写线程或者写请求),那么它可以被授予读可重入的,或者它已经有读访问了(不管写请求)。
去决定一个线程是否有一个读访问了,可以通过在一个Map的key中保持着已经授予读访问的每一个线程的一个引用,对应的value就是它已经获取读锁的次数。当决定这个读访问是否可以被授予的时候,这个Map对于这个正在调用线程的一个引用将会被检查。下面就是lockRead方法和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;
}
}
正如你看到的,如果当前没有线程对这个资源正在写,读重入才可以被授予。另外,如果正在调用的线程已经有读访问了,那么将会优于任何一个写请求。
写可重入(Write Reentrance)
写可重入只是在如果这个线程已经有写访问了才会被授予。这里有一个lockWrite方法和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;
}
}
注意当前持有这个写锁的这个线程是怎么考虑决定正在调用的这个线程是否可以去写访问。
读对于写可重入(Read to Write Reentrance)
有的时候对于已经有一个读访问的一个线程,也会去获取写访问也是需要的。对于这个,这个线程必须只是一个读访问的才会被允许。为了达到这样,这个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;
}
}
现在这个ReadWriteLock类读对于写访问是可重入的。
写对于读可重入的(Write to Read Reentrance)
通常一个获取写访问的一个线程也需要读访问。一个写应该总是被授予读访问如果需要的话。如果一个写访问的线程,没有其他的线程去读或者写,以至于它不是危险的。在这里,canGrantReadAccess方法将会像下面这样修改:
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; } }
完整的可重入锁
下面是一个完整的可重入的ReadWriteLock实现。我做了一些重构对于这个访问条件为了更加容易读。
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;
}
}
从finally子句中调用unlock方法
当在一个ReadWriteLock中控制一个临界区域的时候,这个临界区域可能会抛出异常,在finally子句内部调用readUnlock方法和writeUnlock方法是重要的。这样做可以确保这个ReadWriteLock可以释放锁,以至于其他的锁可以加锁。这里有一个例子:
lock.lockWrite(); try{ //do critical section code, which may throw exception } finally { lock.unlockWrite(); }
这个小的结构可以确保一旦异常抛出,这个ReadWriteLock可以释放锁。如果在finally子句中unlockWrite方法没有被调用,并且一个异常从临界区域中抛出,那么这个ReadWriteLock将会永远持有这个写锁,会导致调用lockRead方法或者lockWrite方法的所有线程将会无限的停止下去。只有如果这个ReadWriteLock是可重入的,才会再一次释放这个锁。当异常抛出的时候,已经锁住的这个线程,后来成功锁住它了,正在执行这个临界区域,后来再一次调用unlockWrite方法。那个将会再一次释放锁。但是为什么要等待那个去发生呢,是否它会发生呢?从finally子句中调用unlockWrite方法是更加健壮的解决方案。
翻译地址:http://tutorials.jenkov.com/java-concurrency/read-write-locks.html