Java中的读写锁(一)

2014-11-24 03:00:30 · 作者: · 浏览: 5

翻译了一篇关于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
  
    readingThreads =
      new HashMap
   
    (); 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