一、回顾基本的读写锁
我们知道读写锁 #java.util.concurrent.locks.ReentrantReadWriteLock
是一个 “读写互斥,写写互斥,读读共享” 的锁。
读写锁的使用非常简单,那就是:
我们只需要保证读锁和写锁来自同一个 ReentrantReadWriteLock
即可,我们知道基于 AQS
实现的锁都是使用一个 原子state
来进行资源控制,那么读写锁是如何去控制这个 原子state
的呢?
注:对重入锁 ReentrantLock
或者 AQS
源码不熟悉的读者往下阅读会有一定的困难,请阅读 图文深入解析Java显式锁底层源码 —— 加解锁是如何实现的 !
二、读写锁概述
如果阅读过前面文章或者说对普通重入锁 #java.util.concurrent.locks.ReentrantLock
有一定了解的小伙伴应该知道,重入锁的实现,就是
1、使用 CAS
对 原子state
进行操作,再根据操作的结果来进行资源控制,且当获取资源失败后,
2、使用 On Sync Queue
来进行阻塞等待排队,并等待唤醒以便进行再次对 原子state
进行 CAS
操作来尝试获取资源
的这么一个反复循环的过程。
这里有一个好消息,读写锁中写锁的资源获取acquire
与释放release
,和重入锁及其类似。读锁在流程上也是分类上面说的两步,但是逻辑则出入较大,不过有了前面的基础,看这篇文章应该不会太吃力。读锁和写锁共用同一个 原子state
和 On Sync Queue
来进行资源控制,那么接下来我们来看看这是如何实现的吧。
三、读锁中对于 原子state 的操作
由于写锁和重入锁基本上是一样的,所以我们先讲读锁。上面说到,读锁的实现也可拆为两个阶段,我们先说说第一个阶段:读锁中对于 原子state
的操作。
读锁中对 原子state
的操作,也就是 tryAcquireShared
方法,我们结合源码和源码中的文档,得出如下三步:
- step1:如果写锁持锁,直接获取资源
acquire
失败(返回 -1),持有写锁的是本线程除外。 - step2:写锁不持锁,则首先根据
queue policy
(公平锁或非公平锁) 判断一下要不要阻塞。不需要阻塞则有其次,通过修改 原子state
来尝试获取资源,成功则要修改一下重入计数。 - step3:上面的都失败了,则进入到
fullTryAcquireShared
中。
protected final int tryAcquireShared(int unused) {
/*
* Walkthrough:
* 1. If write lock held by another thread, fail.
* 2. Otherwise, this thread is eligible for
* lock wrt state, so ask if it should block
* because of queue policy. If not, try
* to grant by CASing state and updating count.
* Note that step does not check for reentrant
* acquires, which is postponed to full version
* to avoid having to check hold count in
* the more typical non-reentrant case.
* 3. If step 2 fails either because thread
* apparently not eligible or CAS fails or count
* saturated, chain to version with full retry loop.
*/
Thread current = Thread.currentThread();
int c = getState();
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
int r = sharedCount(c);
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
}
另外,源码中的文档注释虽然说了不可重入,但实际上是可以重入的,这里简单的说下,如果对写锁(或者说重入锁Reentrantlock
)还有印象的小伙伴应该知道,写锁的重入实际上是对 原子state
进行++操作。而读写则是使用一个 HoldCounter
对象,它的功能很简单,就是负责重入的计数。
但有一个特例,那就是读锁套写锁会死锁,这实际上是读写锁设计上的一个 “缺陷” ,疑问先放在这里,后面我们会娓娓道来。
3.1、原子state 操作之 step1 解析:判断写锁是否已经持锁
这部分逻辑极其简单,但是有一个特殊的设计需要特别关注。
Thread current = Thread.currentThread();
int c = getState();
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
其一,getExclusiveOwnerThread() != current
, exclusiveOwnerThrea