Java实现读写锁的原理 - 极悦
首页 课程 师资 教程 报名

Java实现读写锁的原理

  • 2022-09-23 10:09:08
  • 1587次 极悦

读/写锁是比JavaLock中的锁文本中显示的实现更复杂的锁。想象一下,你有一个应用程序读取和写入一些资源,但写入它的工作不如读取它。读取同一资源的两个线程不会互相造成问题,因此多个想要读取资源的线程同时被授予访问权限,重叠。但是,如果单个线程想要写入资源,则不能同时进行其他读取或写入。为了解决这个允许多个读者但只有一个写者的问题,你需要一个读/写锁。

读/写锁Java实现

首先我们总结一下获取资源读写权限的条件:

读取权限,如果没有线程在写,并且没有线程请求写访问。

写访问,如果没有线程正在读取或写入。

如果一个线程想要读取资源,只要没有线程正在写入,并且没有线程请求对该资源的写访问,就可以了。通过提高写访问请求的优先级,我们假设写请求比读请求更重要。此外,如果读取是最常发生的事情,并且我们没有提高写入的优先级,则可能会发生饥饿。请求写访问的线程将被阻止,直到所有读者都解锁了ReadWriteLock. 如果新线程不断被授予读访问权限,则等待写访问权限的线程将无限期地保持阻塞,从而导致饥饿。因此,如果当前没有线程锁定线程,则只能授予线程读取访问权限ReadWriteLock写作,或要求锁定写作。

可以授予想要对资源进行写访问的线程,因此当没有线程正在读取或写入资源时。有多少线程请求写访问或按什么顺序都没有关系,除非您想保证请求写访问的线程之间的公平性。

考虑到这些简单的规则,我们可以实现ReadWriteLock如下所示:

公共类读写锁{
  私人 int 读者 = 0;
  私人 int 作家 = 0;
  私人 int writeRequests = 0;
  公共同步 void lockRead() 抛出 InterruptedException{
    而(作家> 0 || writeRequests > 0){
      等待();
    }
    读者++;
  }
  公共同步无效解锁读取(){
    读者——;
    通知所有();
  }
  公共同步 void lockWrite() 抛出 InterruptedException{
    写请求++;
    而(读者> 0 ||作家> 0){
      等待();
    }
    写请求——;
    作家++;
  }
  公共同步 void unlockWrite() 抛出 InterruptedException{
    作家——;
    通知所有();
  }
}

有ReadWriteLock两种锁定方法和两种解锁方法。一种用于读取访问的锁定和解锁方法,一种用于写入访问的锁定和解锁方法。

读取访问的规则在该lockRead()方法中实现。所有线程都获得读访问权限,除非有一个线程具有写访问权限,或者一个或多个线程请求了写访问权限。

写访问的规则在lockWrite()方法中实现。想要写访问的线程从请求写访问开始(writeRequests++)。然后它将检查它是否真的可以获得写访问权限。如果没有对资源具有读访问权限的线程,并且没有对资源具有写访问权限的线程,则线程可以获得写访问权限。有多少线程请求写访问并不重要。

值得注意的是,两者都是unlockRead()andunlockWrite()调用 notifyAll()而不是notify(). 要解释为什么会这样,请想象以下情况:

在 ReadWriteLock 内部有等待读访问的线程和等待写访问的线程。如果被唤醒的线程notify()是读访问线程,它将被放回等待,因为有线程在等待写访问。但是,没有一个等待写访问的线程被唤醒,所以没有更多的事情发生。没有线程既不能读也不能写。通过调用noftifyAll()唤醒所有等待的线程并检查它们是否可以获得所需的访问权限。

打电话notifyAll()还有另一个好处。如果多个线程正在等待读取访问,而没有一个线程正在等待写入访问,并且unlockWrite()被调用,则所有等待读取访问的线程都被立即授予读取访问权限 - 而不是一个接一个。

读/写锁重入

前面显示的ReadWriteLock类是不可重入的。如果一个具有写访问权限的线程再次请求它,它将阻塞,因为已经有一个写者——它自己。此外,考虑这种情况:

线程 1 获得读取权限。

线程 2 请求写访问,但由于只有一个读取器而被阻止。

线程1重新请求读访问(重新入锁),但是因为有写请求而被阻塞

在这种情况下,前一个ReadWriteLock会锁定 - 类似于死锁的情况。不会授予既不请求读取也不请求写入访问的线程。

要使ReadWriteLock可重入,有必要进行一些更改。读者和作者的重入将分别处理。

读取重入

为了让ReadWriteLock读者可以重入,我们首先要建立阅读重入的规则:

如果线程可以获得读取访问权限(没有写入者或写入请求),或者如果它已经具有读取访问权限(无论写入请求如何),它就会被授予读取重入权限。

为了确定一个线程是否已经具有读访问权限,对每个被授予读访问权限的线程的引用以及它获得读锁的次数都保存在 Map 中。在确定是否可以授予读取访问权限时,将检查此 Map 是否对调用线程的引用。以下是更改后lockRead()andunlockRead()方法的外观:

公共类读写锁{
  私有 Map<Thread, Integer> readingThreads =
      新的 HashMap<Thread, Integer>();
  私人 int 作家 = 0;
  私人 int writeRequests = 0;
  公共同步 void lockRead() 抛出 InterruptedException{
    线程调用Thread = Thread.currentThread();
    而(!canGrantReadAccess(调用线程)){
      等待();                                                                   
    }
    readingThreads.put(调用线程,
       (getAccessCount(callingThread) + 1));
  }
  公共同步无效解锁读取(){
    线程调用Thread = Thread.currentThread();
    int accessCount = getAccessCount(callingThread);
    if(accessCount == 1){ readingThreads.remove(callingThread); }
    否则 { readingThreads.put(callingThread, (accessCount -1)); }
    通知所有();
  }
  私有布尔canGrantReadAccess(线程调用线程){
    如果(作家> 0)返回假;
    如果(isReader(调用线程)返回真;
    如果(writeRequests > 0)返回假;
    返回真;
  }
  私有 int getReadAccessCount(线程调用线程){
    整数 accessCount = readingThreads.get(callingThread);
    if(accessCount == null) 返回 0;
    返回 accessCount.intValue();
  }
  私有布尔 isReader(线程调用线程){
    返回阅读Threads.get(callingThread) != null;
  }
}

如您所见,仅当当前没有线程写入资源时才授予读取重入。此外,如果调用线程已经具有读取访问权限,则这优先于任何 writeRequests。

写重入

仅当线程已经具有写访问权限时才授予写重入。以下是更改后的lockWrite()andunlockWrite()方法:

公共类读写锁{
    私有 Map<Thread, Integer> readingThreads =
        新的 HashMap<Thread, Integer>();
    私有 int writeAccesses = 0;
    私人 int writeRequests = 0;
    私有线程写作Thread = null;
  公共同步 void lockWrite() 抛出 InterruptedException{
    写请求++;
    线程调用Thread = Thread.currentThread();
    而(!canGrantWriteAccess(调用线程)){
      等待();
    }
    写请求——;
    写访问++;
    写线程 = 调用线程;
  }
  公共同步 void unlockWrite() 抛出 InterruptedException{
    写访问——;
    如果(writeAccesses == 0){
      写线程=空;
    }
    通知所有();
  }
  私有布尔canGrantWriteAccess(线程调用线程){
    如果(hasReaders())返回假;
    if(writingThread == null) 返回真;
    if(!isWriter(callingThread)) 返回假;
    返回真;
  }
  私有布尔 hasReaders(){
    返回读数Threads.size() > 0;
  }
  私有布尔 isWriter(线程调用线程){
    返回写线程 == 调用线程;
  }
}

请注意,在确定调用线程是否可以获得写访问权时,现在如何考虑当前持有写锁的线程。

读写重入

有时,具有读访问权限的线程也需要获得写访问权限。为此,线程必须是唯一的读者。为了实现这一点,writeLock()应该稍微改变方法。这是它的样子:

公共类读写锁{
    私有 Map<Thread, Integer> readingThreads =
        新的 HashMap<Thread, Integer>();
    私有 int writeAccesses = 0;
    私人 int writeRequests = 0;
    私有线程写作Thread = null;
  公共同步 void lockWrite() 抛出 InterruptedException{
    写请求++;
    线程调用Thread = Thread.currentThread();
    而(!canGrantWriteAccess(调用线程)){
      等待();
    }
    写请求——;
    写访问++;
    写线程 = 调用线程;
  }
  公共同步 void unlockWrite() 抛出 InterruptedException{
    写访问——;
    如果(writeAccesses == 0){
      写线程=空;
    }
    通知所有();
  }
  私有布尔canGrantWriteAccess(线程调用线程){
    if(isOnlyReader(callingThread)) 返回真;
    如果(hasReaders())返回假;
    if(writingThread == null) 返回真;
    if(!isWriter(callingThread)) 返回假;
    返回真;
  }
  私有布尔 hasReaders(){
    返回读数Threads.size() > 0;
  }
  私有布尔 isWriter(线程调用线程){
    返回写线程 == 调用线程;
  }
  私有布尔 isOnlyReader(线程线程){
      返回读数Threads.size() == 1 &&
             readingThreads.get(callingThread) != null;
      } 
}

现在ReadWriteLock该类是读写访问可重入的。

写读重入

有时,具有写访问权限的线程也需要读访问权限。如果请求,应始终授予写入者读取访问权限。如果一个线程有写访问权限,其他线程就不能有读或写访问权限,所以它并不危险。以下是该 canGrantReadAccess()方法在更改后的外观:

公共类读写锁{
    私有布尔canGrantReadAccess(线程调用线程){
      if(isWriter(callingThread)) 返回真;
      如果(写线程!= null)返回false;
      如果(isReader(调用线程)返回真;
      如果(writeRequests > 0)返回假;
      返回真;
    }
}

完全可重入读写锁

下面是完全可重入的ReadWriteLock实现。我对访问条件进行了一些重构,以使它们更易于阅读,从而更容易说服自己它们是正确的。

公共类读写锁{
  私有 Map<Thread, Integer> readingThreads =
       新的 HashMap<Thread, Integer>();
   私有 int writeAccesses = 0;
   私人 int writeRequests = 0;
   私有线程写作Thread = null;
  公共同步 void lockRead() 抛出 InterruptedException{
    线程调用Thread = Thread.currentThread();
    而(!canGrantReadAccess(调用线程)){
      等待();
    }
    readingThreads.put(调用线程,
     (getReadAccessCount(callingThread) + 1));
  }
  私有布尔canGrantReadAccess(线程调用线程){
    if( isWriter(callingThread) ) 返回真;
    if( hasWriter() ) 返回假;
    if( isReader(callingThread) ) 返回真;
    if( hasWriteRequests() ) 返回假;
    返回真;
  }
  公共同步无效解锁读取(){
    线程调用Thread = Thread.currentThread();
    如果(!isReader(调用线程)){
      throw new IllegalMonitorStateException("调用线程没有" +
        " 持有此 ReadWriteLock 的读锁");
    }
    int accessCount = getReadAccessCount(callingThread);
    if(accessCount == 1){ readingThreads.remove(callingThread); }
    否则 { readingThreads.put(callingThread, (accessCount -1)); }
    通知所有();
  }
  公共同步 void lockWrite() 抛出 InterruptedException{
    写请求++;
    线程调用Thread = Thread.currentThread();
    而(!canGrantWriteAccess(调用线程)){
      等待();
    }
    写请求——;
    写访问++;
    写线程 = 调用线程;
  }
  公共同步 void unlockWrite() 抛出 InterruptedException{
    if(!isWriter(Thread.currentThread()){
      throw new IllegalMonitorStateException("调用线程没有" +
        " 持有这个 ReadWriteLock 的写锁");
    }
    写访问——;
    如果(writeAccesses == 0){
      写线程=空;
    }
    通知所有();
  }
  私有布尔canGrantWriteAccess(线程调用线程){
    if(isOnlyReader(callingThread)) 返回真;
    如果(hasReaders())返回假;
    if(writingThread == null) 返回真;
    if(!isWriter(callingThread)) 返回假;
    返回真;
  }
  私有 int getReadAccessCount(线程调用线程){
    整数 accessCount = readingThreads.get(callingThread);
    if(accessCount == null) 返回 0;
    返回 accessCount.intValue();
  }
  私有布尔 hasReaders(){
    返回读数Threads.size() > 0;
  }
  私有布尔 isReader(线程调用线程){
    返回阅读Threads.get(callingThread) != null;
  }
  私有布尔 isOnlyReader(线程调用线程){
    返回读数Threads.size() == 1 &&
           readingThreads.get(callingThread) != null;
  }
  私有布尔 hasWriter(){
    返回写作线程!= null;
  }
  私有布尔 isWriter(线程调用线程){
    返回写线程 == 调用线程;
  }
  私有布尔 hasWriteRequests(){
      返回 this.writeRequests > 0;
  }
}

从 finally 子句调用 unlock()

当用 保护临界区时ReadWriteLock,临界区可能会抛出异常,从 - 子句内部调用readUnlock()和writeUnlock()方法很重要finally。这样做可以确保ReadWriteLock已解锁,以便其他线程可以锁定它。这是一个例子:

lock.lockWrite();
尝试{
  //做临界区代码,可能会抛出异常
} 最后 {
  lock.unlockWrite();
}

这个小结构确保ReadWriteLock在关键部分的代码抛出异常的情况下解锁。如果unlockWrite() 没有从 - 子句内部调用finally,并且从临界区抛出异常,ReadWriteLock则将永远保持写锁定,导致调用该实例的所有线程lockRead()或lockWrite()在该 ReadWriteLock实例上无限期停止。唯一可以解锁的ReadWriteLock方法是如果 ReadWriteLock是可重入的,并且在抛出异常时锁定它的线程后来成功锁定它,执行关键部分并unlockWrite() 随后再次调用。那将ReadWriteLock再次解锁。但为什么要等到这种情况发生,如果它发生了吗?unlockWrite()从 -子句调用finally是一个更健壮的解决方案。

以上就是关于“Java实现读写锁的原理”介绍,大家如果想了解更多相关知识,不妨来关注一下本站的Java极悦在线学习,里面的课程内容从入门到精通,细致全面,很适合没有基础的小伙伴学习,希望对大家能够有所帮助。

选你想看

你适合学Java吗?4大专业测评方法

代码逻辑 吸收能力 技术学习能力 综合素质

先测评确定适合在学习

在线申请免费测试名额
价值1998元实验班免费学
姓名
手机
提交