Java中的读写锁模拟实现

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

       读/写锁比起”Java中的锁“一文来的更加深奥。想象着你有一个读/写某些资源的应用程序,并且其中的写操作的执行次数不如读操作多。两个读取相同资源的线程是不会为彼此造成麻烦的,同样地多个线程读取相同资源的线程同时进行读操作也没有问题的。但当某个单独的线程要对资源进行写操作时,必须要求没有其他的线程在对该资源进行读操作或写操作。为了解决多线程读操作,单线程写操作的问题。你需要一个读/写锁。

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

       下面是本文的主题列表: 

       1.读/写锁的Java实现

       2.读写锁的重进入

       3.读的重进入

       4.写的重进入

       5.从读到写的重进入

       6.从写到读的重进入

       7.完整的重进入ReadWriteLock类

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

       读/写锁的Java实现

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

       读访问:没有线程正在进行写操作,同时没有线程已经请求了写操作。(译注:请求写操作(request write access),指的是某个希望进行写操作的线程,已经进入了准备状态,但是可能会有其他线程正在进行写操作,于是当前请求线程只能暂时等待,处于一种”请求了,但是还未开始执行的状态”)

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

       如果一个线程要读取某资源,在没有线程正在对该线程进行写操作,并且也有线程对该资源进行了请求的前提下是可以的。就优先级而言写操作请求高于读操作请求。否则,假如大多数情况下进行的都是读访问,同时又没有对写访问进行优先级处理,就会发生线程饥饿。请求写访问的线程会被阻塞,直至所有的读访问都解锁了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,终于翻完了。说实话笔者也已经翻的有点迷糊了,好在东西不难,只是有些名词比较拗口,简单概括一下:如果程序中读操作比写操作频繁的多,那么写操作的优先级高于读操作。又因为是多线程环境下,显然不能同时有多个线程对同一个资源进行写操作。那么就有些线程变成了”请求了写操作,但是还在等待锁的状态”,显然这些”等待中”的线程的优先级还是高于读操作的线程。于是便可以有一句话的总结,当没有想要进行写操作(包括正在进行写操作和等待进行写操作)的线程存在时,读操作是可以多个同时进行的;当有读操作正在进行读操作时,可以请求写操作,但是不能进行写操作。That’s all.

       想着现在还是改天写个测试用例吧,那样应该更简单易懂一点。

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

       谢谢!!

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

    原文作者:java锁
    原文地址: https://blog.csdn.net/u011638883/article/details/18605761
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞