Java中的读写锁

       翻译了一篇关于Java读写锁的文章,因为笔者之前也没有看过读写锁的相关内容,这里就算是边学习边翻译了,翻的可能不尽准确,高手见谅!好了,闲话少说,进入正题吧。

       读/写锁比起"Java中的锁"一文来的更加深奥。想象着你有一个读/写某些资源的应用程序,而且其中的写操作不如读操作的次数多。两个读取相同资源的线程是不会引发问题的,那么同样地多个线程也应该是可以并行的访问资源的。但是当某个线程要对资源进行写操作时,其他的任意读还是写操作都不应同时处理。为了实现每次只有一个线程进行写操作,多个线程进行读操作。你需要一个读/写锁。

       Java 5在java.util.concurrent包中提供了读/写锁的实现。尽管如此,了解读/写锁背后的原理还是很有用的。

       下面是本文的主题列表: 

       1.读/写锁的Java实现

       2.读写锁的重进入

       3.读的重进入

       4.写的重进入

       5.从读到写的重进入

       6.从写到读的重进入

       7.完整的重进入ReadWriteLock类

       8.在finally语句中调用unlock()

       读/写锁的Java实现

       首先让我们来总结一下进行资源的读访问/写访问的条件。

       读访问:假如没有线程正在进行写访问,并且也没有线程要进行写访问。(译注:读-读操作是不冲突的)

       写访问:没有线程正在进行读访问或写访问。(译注:写-写、写-读操作都是冲突的)

       如果一个线程要读取某资源,只要没有线程正在对该资源进行写访问或者已经请求了写访问的情况下都是可以的。就优先级而言我们指定写操作请求高于读操作请求。否则如果大多数情况下进行的都是读访问,并且又没有优先处理写访问,那么就会发生线程饥饿。请求写访问的线程会被阻塞,直至所有的读访问都解锁了ReadWriteLock。假如新线程不断的进行读操作,那么进行要进行写访问的线程将陷入无限的等待,结果就是线程饥饿。所以一个线程只有在没有被写访问锁定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()方法实现。一个线程要进行写操作必须先进行写访问"请求"动作。"请求"动作会先检查当前线程是否能真的进行写操作。一个线程只有在没有其他线程正在进行读操作或写操作的时候才能进行写操作。不管有多少个线程已经请求了写操作是无关的。

       在unlockRead()和unlockWrite()方法中都使用notifyAll()而不是notify()是有道理的,至于为什么可以想象一下下面的情况:

       在ReadWriteLock的内部有一些线程在等待读访问,另一些线程在等待写访问。现在假如有一个等待读访问的线程被notify()唤醒,那么它只能继续等待,因为有其他请求了写访问的线程存在,什么也不会发生。没有线程获得了读访问或写访问。但是通过调用notifyAll()所有的线程都会被唤醒以检查他们是否可以获取到它们想要的访问权限。

        使用notifyAll()还有另外的一个好处。假如有很多的线程在等待读访问,同时没有等待写访问的线程存在。当unlockWrite()方法被调用后,所有等待读访问的线程会被一次性唤醒,而不是一个一个的唤醒。

        读写锁的重进入

        因为没有考虑重进入所以ReadWriteLock类显得简单了一些。假如一个已经有写访问权限的线程再次请求写访问,该线程就会被阻塞,因为已经存在一个写操作者了--它自己。更具体的可以考虑以下情况:

        1.线程一拿到读访问权限。

        2.线程二请求写访问权限,但是因为已经存在一个读操作者,它被阻塞了。

        3.线程一再次请求读访问(重进入锁),但是它也被阻塞了,因为已经有一个写访问请求的线程存在了。

        这种情况下,前文的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;
  }

}

       如你所见,读重进入只有在当前没有线程对资源进行写操作时才可能被允许。另外,假如调用线程对象已经有了读访问权限,那么它的读重进入优先级将高于任何写访问请求。

       写的重进入

       写的重进入只有在调用线程对象已经拥有了写访问权限时才是被允许的。下面是修改了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;
  }
}

      注意当判断调用线程对象是否可以获得写访问权限时,该线程所持有的写操作锁是要被进行计数的。(这句真心觉得真心拗口,不会翻,帖下原句,知道的朋友告诉一下,先谢了! Notice how the thread currently holding the write lock is now taken into account when determining if the calling thread can get write access.)

       从读到写的重进入

       某些时候一个持有读访问权限的线程需要同时持有写访问权限。这种情况只有在该线程是唯一的读操作者时才是有可能的。为了实现该功能,需要稍微修改一下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;
  }

}

       在finally语句中调用unlock()方法

       当使用ReadWriteLock来保护一个关键的代码片段,同时该关键片段有可能抛出异常时,在finally语句中调用readUnlock()和writeUnlock()方法就显得很重要了。这样做是为了确保对应的ReadWriteLock被解锁(unlocked),同时其他的线程才能对其进行锁定。下面是一个例子:

lock.lockWrite();
try{
  //do critical section code, which may throw exception
} finally {
  lock.unlockWrite();
}

       上面这个小的代码结构保证了当关键代码抛出异常时能够将对应的ReadWriteLock对象解锁。假如在finally语句中没有调用unlockWrite(),同时有一个异常被从关键代码中抛出,这个ReadWriteLock对象就会永远保持者写操作锁定的状态。这会导致其他的调用该ReadWriteLock对象的lockRead()或lockWrite()方法的线程阻塞。只有当该ReadWriteLock被重进入时才有可能解锁,同时那个抛出异常使它锁定的线程会在之后又锁定它,然后在执行关键代码并解锁它。这是一种解锁的可能,但是为什么要等待这种情况的发生呢?直接在finally语句中调用unlockWrite()方法是一种更健壮的方案。

       OK,终于翻完了。说实话笔者也已经翻的有点迷糊了,好在东西不难,只是有些名词比较拗口。想着现在还是改天写个测试用例吧,那样应该更简单易懂一点。

       装载请保留出处:http://blog.csdn.net/u011638883/article/details/18605761

       谢谢!!

       原文地址:http://tutorials.jenkov.com/java-concurrency/read-write-locks.html

Java中的读写锁

上一篇:Restore IP Addresses


下一篇:SAXParser线程变量化提高xml解析性能和吞吐量