Java中的读/写锁
读/写锁定比Java锁定文本中所示的Lock
实现更复杂。假设我们有一个读取和写入某些资源的应用程序,但是编写它的工作并没有读取它那么多。读取相同资源的两个线程不会彼此造成问题,因此,希望读取资源的多个线程同时被授予访问权限,这是相互重叠的。但是,如果单个线程想要写入资源,则不必同时进行其他读取或者写入。要解决允许多个读者但只有一个作者的问题,我们将需要一个读/写锁。
Java 5在java.util.concurrent
软件包中提供了读/写锁定实现。即使这样,了解其实现背后的理论可能仍然有用。
读/写锁Java实现
首先,让我们总结一下获得对该资源的读写访问权的条件:
读访问权限</ b> | 如果没有线程正在写,并且没有线程请求写访问权限。 |
写访问权限</ b> | 如果没有线程正在读取或者写入。 |
如果线程想要读取资源,则可以,只要没有线程正在写入该资源,并且没有线程请求对该资源进行写访问。通过优先处理写访问请求,我们假设写请求比读请求更重要。此外,如果读取是最常发生的事情,并且我们没有对写入进行优先级排序,则可能会发生饥饿。直到所有阅读器都解锁了" 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()"唤醒的线程是读访问线程,则将其放回等待状态,因为有线程在等待写访问。但是,所有等待写访问的线程都不会被唤醒,因此什么也没有发生。没有线程不会获得读或者写访问权限。通过调用noftifyAll()
,所有等待的线程被唤醒,并检查它们是否可以获得所需的访问权限。
调用notifyAll()
还有另一个好处。如果有多个线程正在等待读取访问,而没有一个线程正在等待写入访问,并且调用了unlockWrite(),则所有等待读取访问的线程都会被一次授予读取访问权限,而不是一一对应。
读/写锁重入
前面显示的ReadWriteLock
类不是可重入的。如果具有写访问权的线程再次请求它,则它将阻塞,因为本身已经有一个写程序。此外,请考虑这种情况:
- 线程1获得读取访问权限。
- 线程2请求写访问权限,但由于有一个读取器而被阻止。
- 线程1重新请求读取访问权限(重新输入锁),但由于存在写请求而被阻止
在这种情况下,先前的" ReadWriteLock"将锁定类似于死锁的情况。没有请求读取或者写入访问权限的线程都不会被授予。
为了使" ReadWriteLock"可重入,必须进行一些更改。对于读者和作家的重入将单独处理。
阅读重入
为了使读者能够重读" ReadWriteLock",我们将首先建立读取重入的规则:
- 如果线程可以获取读访问权限(没有写程序或者写请求),或者已经拥有读访问权限(无论写请求如何),则授予该线程读重入权限。
为了确定线程是否已经具有读取访问权限,将对每个授予读取访问权限的线程的引用及其已获得读取锁定的次数保留在Map中。在确定是否可以授予读取访问权限时,将检查此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; } }
如我们所见,读重入仅在当前没有线程写入资源时才被授予。此外,如果调用线程已经具有读取访问权限,则此优先级高于所有writeRequests。
写重入
仅当线程已经具有写访问权限时,才授予写重入权限。这是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; } }
注意,在确定调用线程是否可以进行写访问时,现在如何考虑当前持有写锁的线程。
读写重入
有时,具有读取访问权限的线程也必须获得写入访问权限。为此,线程必须是唯一的读取器。为了达到这个目的,应该稍微修改一下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"类是读写访问可重入的类。
写读重入
有时具有写访问权的线程也需要读访问权。如果有要求,应始终授予写者读访问权限。如果一个线程具有写访问权限,则其他线程都不能具有读或者写访问权限,因此这并不危险。更改后的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
下面是完全重新进入的" 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; } }
从最后一个子句中调用unlock()
当用ReadWriteLock
保护关键部分时,关键部分可能会抛出异常,重要的是从'最终'子句内部调用readUnlock()
和writeUnlock()
方法。这样做可以确保" ReadWriteLock"已解锁,以便其他线程可以锁定它。这是一个例子:
lock.lockWrite(); try{ //do critical section code, which Jan throw exception } finally { lock.unlockWrite(); }
这个小结构可以确保在关键部分的代码抛出异常的情况下将" ReadWriteLock"解锁。如果未从"最终"子句中调用" unlockWrite()",并且从关键部分引发了异常,则" ReadWriteLock"将永远保持写锁定状态,从而导致所有调用" lockRead()"或者" lockWrite"的线程()在该ReadWriteLock实例上无限期停止。再次可以解锁ReadWriteLock的唯一事情是,如果ReadWriteLock是可重入的,并且在引发异常时锁定了它的线程,后来成功锁定了它,执行了关键部分并调用了unlockWrite()
之后再说一次。那将再次解锁ReadWriteLock
。但是,为什么要等它发生呢?从"最终"子句中调用" unlockWrite()"是一种更为健壮的解决方案。