首页 文章详情

面试官:说说读写锁的底层实现?

Java中文社群 | 80 2024-06-02 13:20 0 0 0
UniSMS (合一短信)

大家好,我是磊哥,今天是一篇硬核文,请各位读者大大们系好安全带,马上要发车了。

a3426497b66cfa2be7705c511b970b1b.webp

晕车的朋友,可以先吃一颗阿星独家秘制的晕车药,童叟无欺,货真价实。

本文大纲如下

cdf29cea9737591074db6a2c87c26870.webp 纵观全局

我的英文名叫 ReentrantReadWriteLock (后面简称 RRW ),大家喜欢叫我读写锁,因为我常年混迹在读多写少的场景。

读写锁规范

作为合格的读写锁,先要有读锁与写锁才行。

所以声明了ReadWriteLock接口,作为读写锁的基本规范。

718e59d33bbc01d6c8f2c7671c6b68b2.webp

之后都是围绕着规范去实现读锁与写锁。

读锁与写锁

WriteLock与ReadLock就是读锁和写锁,它们是RRW实现 ReadWriteLock 接口的产物。

但读锁、写锁也要遵守锁操作的基本规范。

所以WriteLock与ReadLock都实现了Lock接口。

f6070d44148a2fc5d86b24154a772346.webp

那么WriteLock与ReadLock对Lock接口具体是如何实现的呢?

自然是少不了我们的老朋友AQS了。

AQS

众所周知,要实现锁的基本操作,必须要仰仗AQS老大哥了。

AQS(AbstractQueuedSynchronizer)抽象类定义了一套多线程访问共享资源的同步模板,解决了实现同步器时涉及的大量细节问题,能够极大地减少实现工作,用大白话来说,AQS为加锁和解锁过程提供了统一的模板函数,只有少量细节由子类自己决定。

AQS简化流程图如下

e3dc026d3fc9940a713aba1260311631.webp

如果读者想深入AQS细节,可以看阿星的这篇文章:16张图揭开AQS

Sync

AQS为加锁和解锁过程提供了统一的模板函数,只有少量细节由子类自己决定,但是WriteLock与ReadLock没有直接去继承AQS

因为WriteLock与ReadLock觉得,自己还要去继承AQS实现一些两者可以公用的抽象函数,不仅麻烦,还有重复劳动。

所以干脆单独提供一个对锁操作的类,由WriteLock与ReadLock持有使用,这个类叫Sync

Sync继承AQS实现了如下的核心抽象函数

  • tryAcquire
  • release
  • tryAcquireShared
  • tryReleaseShared
dfef3f21fc1a78ea86dd73da6989e02a.webp

其中tryAcquire、release是为WriteLock写锁准备的。

tryAcquireShared、tryReleaseShared是为ReadLock读锁准备的,这里阿星后面会说。

上面说了Sync实现了一些AQS的核心抽象函数,但是Sync本身也有一些重要的内容,看看下面这段代码

ebb9a3381dcb65f1d6a1f96675c19617.webp

我们都知道AQS中维护了一个state状态变量,正常来说,维护读锁与写锁状态需要两个变量,但是为了节约资源,使用高低位切割实现state状态变量维护两种状态,即高16位表示读状态,低16位表示写状态。

关于读写锁状态设计具体细节可以看阿星的文章:ReentrantReadWriteLock的位运算

Sync中还定义了HoldCounter与ThreadLocalHoldCounter

  • HoldCounter是用来记录读锁重入数的对象
  • ThreadLocalHoldCounter是ThreadLocal变量,用来存放第一个获取读锁线程外的其他线程的读锁重入数对象

3ab690fd291a8ebf4d465d5fdae12474.webp


如果读者对ThreadLocal不太熟悉,可以去看阿星的文章: 保姆级教学,22张图揭开ThreadLocal

公平与非公平策略

你看,人家ReentrantLock都有公平与非公平策略,所以ReentrantReadWriteLock也要有。

什么是公平与非公平策略?

因为在AQS流程中,获取锁失败的线程,会被构建成节点入队到CLH队列,其他线程释放锁会唤醒CLH队列的线程重新竞争锁,如下图所示(简化流程)。

b070ce6295fa64489641915212b88a70.webp

非公平策略是指,非CLH队列的线程与CLH队列的线程竞争锁,大家各凭本事,不会因为你是CLH队列的线程,排了很久的队,就把锁让给你。

公平策略是指,严格按照CLH队列顺序获取锁,一定会让CLH队列线程竞争成功,如果非CLH队列线程一直占用时间片,那就一直失败,直到时间片轮到CLH队列线程为止,所以公平策略的性能会更差。

a67d4fe42a69ea08918aca58345020ea.webp

回到正题,为了支持公平与非公平策略,Sync扩展了FairSync、NonfairSync子类,两个子类实现了readerShouldBlock、writerShouldBlock函数,即读锁与写锁是否阻塞

6b252bdda0de5d9ae1514f55158369d3.webp

readerShouldBlock、writerShouldBlock函数在什么地方使用阿星后面会说。

ReentrantReadWriteLock全局图

最后阿星把前面讲过的内容,全部组装起来,构成下面这张图。

3dee3e17d86f387cd6710be17f643498.webp

有了全局观后,后面就可以深入细节逐个击破了。

深入细节

后面我们只要攻破5个细节就够了,分别是读写锁的创建、获取写锁、释放写锁、获取读锁、释放读锁。

ReentrantReadWriteLock的创建

读写锁的创建,会初始化化一系列类,代码如下

23a6b87ffc8c28ef24e891591f7cdbbf.webp

ReentrantReadWriteLock默认是非公平策略,如果想用公平策略,可以直接调用有参构造器,传入true即可。

但不管是创建FairSync还是NonfairSync,都会触发Sync的无参构造器,因为Sync是它们的父类(本质上它们俩都是Sync)。

0253f9b8b1e951757252998c849ab89c.webp

因为Sync需要提供给ReadLock与WriteLock使用,所以创建ReadLock与WriteLock时,会接收ReentrantReadWriteLock对象作为入参。

a68242c83994e6576cb1abd820f434d7.webp

最后通过ReentrantReadWriteLock.syncSync交给了ReadLock与WriteLock。

获取写锁

我们遵守ReadWriteLock接口规范,调用ReentrantReadWriteLock.writeLock函数获取写锁对象。

580916d2da7d046fe73278d105a7eab6.webp

获取到写锁对象后,遵守Lock接口规范,调用lock函数获取写锁。

WriteLock.lock函数是由Sync实现的(FairSync或NonfairSync)。

34c8ae46d8b8f6c49302824fe423012c.webp

sync.acquire(1)函数是AQS中的独占式获取锁流程模板(Sync继承自AQS)。

7f7ab485bd3e6dfbbc7c67ddc6b54b35.webp

WriteLock.lock调用链如下图

689fcc4133e9de5c58f98fb84f8e77b5.webp

我们只关注tryAcquire函数,其他函数是AQS的获取独占式锁失败后的流程内容,不属于本文范畴,tryAcquire函数代码如下

e95fdca48c29c316a2f0fe573b5b0e5b.webp

为了易于理解,阿星把它转成流程图

0b019f8c72ed83759be162991cee9e44.webp

通过流程图,我们发现了一些要点

  • 读写互斥
  • 写写互斥
  • 写锁支持同一个线程重入
  • writerShouldBlock写锁是否阻塞实现取决公平与非公平的策略(FairSync和NonfairSync)

释放写锁

获取到写锁,临界区执行完,要记得释放写锁(如果重入多次要释放对应的次数),不然会阻塞其他线程的读写操作,调用unlock函数释放写锁(Lock接口规范)。

WriteLock.unlock函数也是由Sync实现的(FairSync或NonfairSync)。

c10d3290c531ea3300ae0ecfed669d69.webp

sync.release(1)执行的是AQS中的独占式释放锁流程模板(Sync继承自AQS)。

7f270a4223f014a3a8d0a78ffd521518.webp

WriteLock.unlock调用链如下图

e5dc6b9a745c261e71ba3192ee8513f1.webp

再来看看tryRelease函数,其他函数是AQS的释放独占式成功后的流程内容,不属于本文范畴,tryRelease函数代码如下

f73472c2e982eece76048a037e0123fb.webp

为了易于理解,阿星把它转成流程图

0a5acf6f3351c92680738b78b05bc27e.webp

因为同一个线程可以对相同的写锁重入多次,所以也要释放的相同的次数。

获取读锁

我们遵守ReadWriteLock接口规范,调用ReentrantReadWriteLock.readLock函数获取读锁对象。

d2344cb2c3347100ccca7cc96b0b1ec4.webp

获取到读锁对象后,遵守Lock接口规范,调用lock函数获取读锁。

ReadLock.lock函数是由Sync实现的(FairSync或NonfairSync)。

1bb9b35623e6d11e4bdb9eab023f1eb5.webp

sync.acquireShared(1)函数执行的是AQS中的共享式获取锁流程模板(Sync继承自AQS)。

f28528ca2664a9d373544f2365f95022.webp

ReadLock.lock调用链如下图

d99e3b84e9441d621fc1dfd5c2645647.webp

我们只关注tryAcquireShared函数,doAcquireShared函数是AQS的获取共享式锁失败后的流程内容,不属于本文范畴,tryAcquireShared函数代码如下

c8618682ee55a20a856c92995b70fa96.webp

代码还挺多的,为了易于理解,阿星把它转成流程图

31d1518c2798d17d34fce36548d15d52.webp

通过流程图,我们发现了一些要点

  • 读锁共享,读读不互斥
  • 读锁可重入,每个获取读锁的线程都会记录对应的重入数
  • 读写互斥,锁降级场景除外
  • 支持锁降级,持有写锁的线程,可以获取读锁,但是后续要记得把读锁和写锁读释放
  • readerShouldBlock读锁是否阻塞实现取决公平与非公平的策略(FairSync和NonfairSync)

释放读锁

获取到读锁,执行完临界区后,要记得释放读锁(如果重入多次要释放对应的次数),不然会阻塞其他线程的写操作,通过调用unlock函数释放读锁(Lock接口规范)。

ReadLock.unlock函数也是由Sync实现的(FairSync或NonfairSync)。

4a6a30cb8330b6c778f414c1749c4f9d.webp

sync.releaseShared(1)函数执行的是AQS中的共享式释放锁流程模板(Sync继承自AQS)。

679e16297b8066a54e28d6b3e024b0ff.webp

ReadLock.unlock调用链如下图

1b41f9e78f6c8837ae6e392e4eb74e07.webp

我们只关注tryReleaseShared函数,doReleaseShared函数是AQS的释放共享式锁成功后的流程内容,不属于本文范畴,tryReleaseShared函数代码如下

2f0b053e7411eb397a6bee42dd011583.webp

为了易于理解,阿星把它转成流程图

c67c0e80fd3c6f226f97f54c12d325d3.webp

这里有三点需要注意

  • 第一点:线程读锁的重入数与读锁数量是两个概念,线程读锁的重入数是每个线程获取同一个读锁的次数,读锁数量则是所有线程的读锁重入数总和。

  • 第二点:AQS的共享式释放锁流程模板中,只有全部的读锁被释放了,才会去执行doReleaseShared函数

  • 第三点:因为使用的是AQS共享式流程模板,如果CLH队列后面的线程节点都是因写锁阻塞的读锁线程节点,会传播唤醒

小结

最后阿星做个小结,ReentrantReadWriteLock底层实现与ReentrantLock思路一致,它们都离不开AQS,都是声明一个继承AQSSync,并在Sync下扩展公平与非公平策略,后续的锁相关操作都委托给公平与非公平策略执行。

我们还发现,在AQS中除了独占式模板,还有共享式模板,它们在多线程访问共享资源的流程会有所差异,就如ReentrantReadWriteLock中读锁使用共享式,写锁使用独占式。

最后再捋一捋写锁与读锁的逻辑

  1. 读读不阻塞
  2. 写锁阻塞写之后的读写锁,但是不阻塞写锁之前的读锁线程
  3. 写锁会被写之前的读写锁阻塞
  4. 读锁节点唤醒会无条件传播唤醒CLH队列后面的读锁节点
  5. 写锁可以降级为读锁,防止更新丢失
  6. 读锁、写锁都支持重入

说件大事

磊哥一直在做的事,为了让大家找到更好的工作,所以有着 14 年工作经验(包含 9 年面试官经验)和 4 年就业辅导经验的我,开发了一门《Java 面试突击训练营》。

课程是通过视频的方式,把 Java 常见的面试题系统的过一遍遇到一个问题,把这个问题相关的内容都给大家讲明白,并且视频支持永久观看和一直更新。训练营还配套了 10 大就业服务(一对一模拟面试、简历辅导、一对一答疑、Offer 选择等)。 

课程涉及的模块如下:

379c504fe935b0150485643b51683907.webp

训练营带你高效的搞定面试问题,学完之后可以应对当前市面上绝大部分公司的面试,得到高薪 Offer。

加我微信咨询吧:GG_Stone【备注:训练营】

1c09258d66015d6edc77939cc9a33638.webp

good-icon 0
favorite-icon 0
收藏
回复数量: 0
    暂无评论~~
    Ctrl+Enter