并发王者课-铂金2:豁然开朗-“晦涩难懂”的ReadWriteLock竟如此妙不可言

Posted Java老猿

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了并发王者课-铂金2:豁然开朗-“晦涩难懂”的ReadWriteLock竟如此妙不可言相关的知识,希望对你有一定的参考价值。

在本文中,我们将介绍Java中锁的另外一个重要的基本型接口,即ReadWriteLock接口。

在探索Java中的并发时,ReadWriteLock无疑是重要的,然而理解它却并不容易。如果你此前曾经检索资料,应该会发现大部分的文章对它的描述都比较晦涩难懂,或连篇累牍的源码陈列,或隔靴搔痒的三言两语,既说不到重点,也说不清来龙去脉。

所以,在本文中我们会将介绍的重点放在对思路的理解上,而不是对源码的解读上。对于源码以及其背后的知识,我们将在后面的更高级的系列中进行讲解。

一、理解ReadWriteLock存在的价值

理解ReadWriteLock,首页要理解它存在的意义是什么。换言之,它要解决什么问题。为此,我们不妨从下图着手一探究竟。

img

不知你看明白了没有,这幅图所表达的有三层含义:

  • 大量线程在竞争同一份资源;
  • 这些线程中有的是读请求,有的是写请求
  • 在多个线程的请求中,读请求明显高于写请求

这样的场景是否似曾相识?没错,它就是典型的缓存应用场景

众所周知,缓存的存在是为了提高应用的读写性能。一方面,我们需要通过缓存拦截大量的读数据的请求。另一方面,我们也需要不定期地更新缓存。但总体而言,更新缓存的次数远远小于读缓存的次数

在这个过程中,关键问题在于,为了保持数据一致性,我们在读写缓存的时候,不能让读请求拿到脏数据,这就需要用到锁。然而,更关键的问题在于,虽然读写之间需要互斥,但读与读之间不可以互斥

总结来说,这个问题主要有下面这几个要点:

  • 数据允许多个线程同时读取,但只允许一个线程进行写入
  • 在读取数据的时候,不可以存在写操作或者写请求
  • 在写数据的时候,不可以存在读请求

如果你对此仍然有些迷茫,那么下面这张图建议你收藏,这张图正是ReadWriteLock对问题的概述和它的解决方案,也是诠释ReadWriteLock最好的一幅图。

img

在你没有理解ReadWriteLock之前,你会觉得它十分晦涩且源码枯燥。然而,一旦你理解它要解决的问题,以及它所提供的方案后,你会发现它的设计竟然如此巧妙。它竟然设计了两种截然不同的锁,其中一把正如我们此前认知的那样是线程互斥的,而另一把锁竟然可以为多个线程所共享!两把锁的完美配合,解决了并发读写的场景问题。

在恍然大悟后,所谓源码不过是队列与共享,它们是ReadWriteLock的一种实现方式,而不是阻挡你理解的绊脚石。

二、自主实现ReadWriteLock

在理解了ReadWriteLock背后的问题和它的解决思路之后,我们就可以完全抛开JDK中的源码自己实现一把读写锁。

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();
  }
}

在读锁lockRead()中,是不允许有写请求写操作的。如果有,那么读请求将进入等待。

而在lockWrite()中,同时不允许读请求和其他写操作的存在,此时只允许有一个写请求

以上就是读写锁简单的自主实现方式。当然,它是不完善的,只是基本的示例。它没有考虑到基本的线程重入问题,真实情况也比它复杂很多,但你理解它的意思就好。

三、Java中的ReadWriteLock是如何实现的

最后,我们再来看JDK中的ReadWriteLock实现的一些基本思路。ReadWriteLock和我们上篇所说的Lock接口以及其他类的基本关系如下图所示:

img

可以看到,JDK中的读写锁的实现是在ReentrantReadWriteLock这个类中。ReentrantReadWriteLock包含了两个内部类:ReadLock和WriteLock,而这两个类又实现了Lock接口。

读写锁的升级与降级

读写锁的升级与降级是ReentrantReadWriteLock中的一个重要知识点,也是高频的面试题。

从读锁到写锁,称之为锁的升级,反之为锁的降级。理解读写锁的升级和降级,最直观的方式是写代码验证。

代码片段1,先获取读锁,再获取写锁。

public class ReadWriteLockDemo {
    public static void main(String[] args) {
        ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
        readWriteLock.readLock().lock();
        System.out.println("已经获取读锁...");
        readWriteLock.writeLock().lock();
        System.out.println("已经获取写锁...");
    }
}

输出结果如下:

已经获取读锁...

代码片段2,先获取写锁,再获取读锁:

public class ReadWriteLockDemo {
    public static void main(String[] args) {
        ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
        readWriteLock.writeLock().lock();
        System.out.println("已经获取写锁...");
        readWriteLock.readLock().lock();
        System.out.println("已经获取读锁...");
    }
}

输出结果如下:

已经获取写锁...
已经获取读锁...

Process finished with exit code 0

这样一来,结果已经十分明了。ReentrantReadWriteLock支持锁的降级,但不支持锁的升级

读写锁中的公平性

在前面的文章中,我们讲过线程饥饿的由来和后果,所以良好的并发工具类在设计时都会考虑到公平性,ReentrantReadWriteLock也是如此。

在ReentrantReadWriteLock中,同时提供了公平和非公平两种模式,且默认为非公平模式。从下面摘取的源码片段中,可以清晰地看到。

 public ReentrantReadWriteLock() {
        this(false);
 }

    /**
   /**
 * Creates a new {@code ReentrantReadWriteLock} with
 * default (nonfair) ordering properties.
 */
public ReentrantReadWriteLock() {
  this(false);
}

/**
 * Creates a new {@code ReentrantReadWriteLock} with
 * the given fairness policy.
 *
 * @param fair {@code true} if this lock should use a fair ordering policy
 */
public ReentrantReadWriteLock(boolean fair) {
  sync = fair ? new FairSync() : new NonfairSync();
  readerLock = new ReadLock(this);
  writerLock = new WriteLock(this);
}

小结

以上就是关于读写锁的全部内容。在本文中,我们从缓存问题出发,接着从ReadWriteLock中寻找答案,以便能从更轻松的角度理解ReadWriteLock的来龙去脉。

理解ReadWriteLock的关键不在于对源码的剖析,而在于对其思路的理解。

另外,我们简单地介绍了ReentrantReadWriteLock中的一些关键知识点,但诸如其背后的AQS等并没有展开陈述。对此也不必着急,我们会在后面有详细的分析介绍。

小伙伴们,帮忙一键三连呀

题外话,我在一线互联网企业工作十余年里,指导过不少同行后辈。帮助很多人得到了学习和成长。

我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在Java学习中的很多困惑,所以在工作繁忙的情况下还是坚持各种整理和分享。但苦于知识传播途径有限,很多程序员朋友无法获得正确的资料得到学习提升

故此将并将重要的Java进阶资料包括并发编程、JVM调优、SSM、设计模式、spring等知识技术、阿里面试题精编汇总、常见源码分析等录播视频免费分享出来,需要领取的麻烦点这里自行下载或者是添加QQ1404119194,备注csdn

Java进阶视频资料

在这里插入图片描述

Java面试题精编汇总

在这里插入图片描述

JAVA核心知识点整理在这里插入图片描述

在这里插入图片描述

获取方式: 只需你一键三连后,扫描👇二维码自行免费下载或者添加QQ1404119194,备注csdn

在这里插入图片描述

以上是关于并发王者课-铂金2:豁然开朗-“晦涩难懂”的ReadWriteLock竟如此妙不可言的主要内容,如果未能解决你的问题,请参考以下文章

并发王者课-青铜1:牛刀小试-如何创建线程之初体验

并发王者课-青铜6:借花献佛-如何格式化Java内存工具JOL输出

并发王者课-青铜5:一探究竟-如何从synchronized理解Java对象头中的锁

并发王者课-黄金3:雨露均沾-不要让你的线程在竞争中被“饿死”

晦涩难懂的电子邮件脚本

Visual C:命名空间范围错误或晦涩难懂的功能?