ReentrantReadWriteLock源码解析

网友投稿 626 2025-04-09

上回说到ReentrantLock,今天来谈谈读写锁(ReentrantLock)和其具体实现ReentrantReadWriteLock。看这篇文章前,强烈建议你回到先读懂ReentrantLock,因为ReentrantReadWriteLock其实是在ReentrantLock的基础上实现的,可以参考我之前的博客ReentrantLock源码解析


既然有了锁,为什么还需要读写锁?我们来想象下这个场景。你们小区楼下有个公告栏,有时候有人会写个招租,有时候有人会写个寻物启事…… 当然一个人正在改公告栏的时候,另外一个人就不能同时改了,这里就相当于有了一把无形的锁,我改的时候就把广告栏“锁住”,改完再“解锁”,当然别人锁住了之后我也改不了。说完了“写”再说“读”,一个人在读公告栏的时候,别人就不能去写了,这样不礼貌,这里也相当于读的人用一把“锁”把公告栏给锁了。

如果这里读者用的锁和写者用的锁是一样的,那么这把锁不紧不然别人写了,也不让别人读了,相当于一个人在看公告栏,别人就不能看了,这明显不合理啊。 所以要把读和写用的锁区分开来,所有读的人共享一把锁,写的人独享锁。放到公告栏的例子上,改公告的时候同时只有一个人可以看,但读的时候所有人可以同时读,这样就可以把“公告栏”这个资源的利用率最大化。

看到这里,你应该已经理解了什么叫做“读写锁”,接下来我们直接看下jdk中ReentrantReadWriteLock的实现,再次建议先阅读ReentrantLock的具体实现。

从类结构图看,貌似它比ReentrantLock更复杂写,多两个内部类 ReadLock 和 WriteLock,看着Lock提供的api完全一样,看来得从具体实现上来看其二者有什么样的差异了。

public ReentrantReadWriteLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); readerLock = new ReadLock(this); writerLock = new WriteLock(this); }

1

2

3

4

5

6

从ReentrantReadWriteLock的构造方法可以看出,它也支持公平锁和非公平锁,当然默认也是非公平锁。和ReentrantLock一样,加锁和解锁的实现逻辑都是在 Sync 里,所以我们重点看下Sync的实现,代码太多这里就不贴完整代码了,建议读者自行打开代码。

Sync

从Sync的类结构图来看,它还是相当复杂的,别急让我们来捋一捋,我们先从WriteLock看起(看起来会比较熟悉),看下他的lock和release的具体实现。

@ReservedStackAccess final boolean tryWriteLock() { Thread current = Thread.currentThread(); // 1 int c = getState(); // 2 if (c != 0) { // 3 int w = exclusiveCount(c); // 4 if (w == 0 || current != getExclusiveOwnerThread()) // 5 return false; if (w == MAX_COUNT) //6. MAX_COUNT = 65535 throw new Error("Maximum lock count exceeded"); } if (!compareAndSetState(c, c + 1)) // 7 return false; setExclusiveOwnerThread(current); // 8 return true; }

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

如果你看过ReentrantLock的话,相信这段代码你已经完全能看懂了。这里我再大概说下这段代码的流程

获取到当前线程。

获取到锁对象的state值,state是保存了锁的状态。

如果state不为0,说明已经有线程加过锁了,这时候需要额外判断下,跳到4。 如果state为0,直接跳到 7。

获取到当前加写锁的次数,这里获取的是state的低16位。

c已经不为0了,如果w不为0说明有线程加了写锁,如果加了写锁的线程也不是当前线程的,加锁就失败了。

这里需要额外判断下锁重入的次数,如果已经到65535就不能再加锁了,后续会解释为什么是65535。

执行CAS操作更改锁状态 state。

到这里说明加写锁已经成功了,把当前锁的持有者记录下来。

@ReservedStackAccess final boolean tryReadLock() { Thread current = Thread.currentThread(); // 1 for (;;) { int c = getState(); // 2 if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current) // 3 return false; int r = sharedCount(c); // 4 if (r == MAX_COUNT) // 5 throw new Error("Maximum lock count exceeded"); if (compareAndSetState(c, c + SHARED_UNIT)) { // 6 if (r == 0) { firstReader = current; firstReaderHoldCount = 1; } else if (firstReader == current) { firstReaderHoldCount++; } else { HoldCounter rh = cachedHoldCounter; if (rh == null || rh.tid != LockSupport.getThreadId(current)) cachedHoldCounter = rh = readHolds.get(); else if (rh.count == 0) readHolds.set(rh); rh.count++; } return true; } } }

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

读锁的加锁代码就完全不一样了,第一眼看到的不同就是这里有个大大的无限循环,我们还是来看下读锁的加锁过程。

获取当前线程。

获取锁的state状态值。

如果写锁的加锁次数不是0切写锁持有者不是当前线程,加读锁失败。

获取读锁的加锁次数,sharedCount©获取的是state的高16位。

如果读锁加锁次数达到65535,抛Error,和写锁一样,只能加65535次。

执行到这,说明可以加锁,使用CAS更新state成功后这里就开始记录一些读锁的状态信息,注意这里state增加值不是1,而是SHARED_UNIT(65536)。

看完readLock和writeLock的加锁方式就可以大体理解ReentrantReadWriteLock的实现了,原来它只是把ReentrantLock中的state分成两部分来用,高16位记录读锁状态,低16位记录写锁状态,如下图。

这也是为什么上文中加锁最大次数是65535的原因了,这也是而是SHARED_UNIT的值为65536的原因。

理解了加锁的代码,解锁部分也就好理解了,本质上是把加锁的代码反向执行下,代码如下。

@ReservedStackAccess protected final boolean tryRelease(int releases) { if (!isHeldExclusively()) throw new IllegalMonitorStateException(); int nextc = getState() - releases; boolean free = exclusiveCount(nextc) == 0; if (free) setExclusiveOwnerThread(null); setState(nextc); return free; } @ReservedStackAccess protected final boolean tryReleaseShared(int unused) { Thread current = Thread.currentThread(); if (firstReader == current) { // assert firstReaderHoldCount > 0; if (firstReaderHoldCount == 1) firstReader = null; else firstReaderHoldCount--; } else { HoldCounter rh = cachedHoldCounter; if (rh == null || rh.tid != LockSupport.getThreadId(current)) rh = readHolds.get(); int count = rh.count; if (count <= 1) { readHolds.remove(); if (count <= 0) throw unmatchedUnlockException(); } --rh.count; } for (;;) { int c = getState(); int nextc = c - SHARED_UNIT; if (compareAndSetState(c, nextc)) // Releasing the read lock has no effect on readers, // but it may allow waiting writers to proceed if // both read and write locks are now free. return nextc == 0; } }

1

2

3

4

5

6

7

8

9

10

11

12

13

ReentrantReadWriteLock源码解析

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

Sync中还有一个ThreadLocalHoldCounter类,这个类的作用其实是记录每个线程对读锁的加锁测试,见名知意线程级的统计,代码也很简单,这里就不再贴了。

Sync中除了上文说到的几个加解锁的API,其余一些API就是获取Sync对象中各个状态的API,没什么好说的。

FairSync & NonfairSync

说完了抽象类Sync,我们来说下它的两个具体实现 FairSync 和 NonfairSync。 这两个实现类非常非常简单,只是重写了 writerShouldBlock() 和 readerShouldBlock() 方法而已,如果你已经知道什么是公平和非公平了,这地方也就很好理解了。

static final class NonfairSync extends Sync { private static final long serialVersionUID = -8159625535654395037L; final boolean writerShouldBlock() { // 写锁可以始终不被等待队列里的线程阻塞,只要当前锁是未锁定状态就可以加锁 return false; } final boolean readerShouldBlock() { //这个方法判断队列的head.next是否正在等待写锁,这个方法确保读锁不应该让写锁始终等待,即便是非公平的,但写锁有更高的优先级,获取读锁还是得排队。 return apparentlyFirstQueuedIsExclusive(); } } // 公平锁就很好理解了,只要等待队列不为空,就得去排队 static final class FairSync extends Sync { private static final long serialVersionUID = -2274990926593161451L; final boolean writerShouldBlock() { return hasQueuedPredecessors(); } final boolean readerShouldBlock() { return hasQueuedPredecessors(); } }

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

ReadLock & WriteLock

其实看完Sync里的逻辑,基本上ReadLock和WriteLock的实现逻辑我们已经知道了。ReadLock和WriteLock只是向用户提供里有些功能抽象(实现了Lock中的方法),封装好了具体的实现,其实具体逻辑还是在Sync中实现。

从类继承关系来看,二者也只是简单

结论

了解完ReentrantReadWriteLock的实现后你就会发现,它其实和ReentrantLock一样,之前把ReentrantLock中的state切分成两部分用,高16位作为读锁的state,低16位作为写锁。如果把ReadLock和WriteLock拉出来单独看的话,二者都是一个ReentrantLock,只是不能像ReentrantLock那样重入那么多次而已。

ReentrantReadWriteLock的出现大幅提升了多读少写场景下的性能问题,但它依旧有自己的缺点,就是它可能会导致写饥饿。还是拿小区公告栏的例子,如果任意时刻都有人在看公告栏,你也不好打断人家所以你公告更新不了啊,所以想更新的人就得一直等着。

关注我,下次和大家一起看下 StampedLock 是如何解决饥饿问题的。

任务调度

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:请问 怎么 显示制表位?(请问荨麻疹会不会传染)
下一篇:excel删除选定区域(excel删除所选区域的内容)
相关文章