概述
在前文,我们结合 ReentrantLock 分析了 AQS 的独占模式加解锁,而这篇文章则将讲解共享模式的加解锁。
共享模式,即锁资源同时运行被多个线程共享,典型的实现即读写锁 ReentrantReadWriteLock,接下里我们将结合它分析 AQS 中共享模式加解锁的流程。
1.共享锁的数据结构
首先,共享锁的核心是“共享”,这决定了共享锁需要通过一个 state 同时维护多种锁的状态,这也导致了共享锁相对独占锁,其实现类会有比较特殊的数据结构。
以典型的共享锁实现读写锁 ReentrantReadWriteLock 为例,它共有五个内部类:
Sync:核心抽象同步器,继承了AQS,提供基本的加解锁实现,也是最核心的类;NonfairSync:公平锁实现,继承了Sync;FairSync:非公平锁实现,继承了Sync;ReadLock:读锁实现,通过持有一个Sync示例来完成加解锁逻辑;WriteLock:写锁实现,通过持有一个Sync示例来完成加解锁逻辑;
我们重点关注最核心的抽象同步器 Sync,它的数据结构如下:
/**
* Synchronization implementation for ReentrantReadWriteLock.
* Subclassed into fair and nonfair versions.
*/
abstract static class Sync extends AbstractQueuedSynchronizer {
static final int SHARED_SHIFT = 16;
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
// 获取共享锁(读锁)加锁次数
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
// 获取独占锁(写锁)加锁次数
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
// 同步状态 & 锁状态
private volatile int state;
// 读锁计数器
private transient ThreadLocalHoldCounter readHolds;
// 当前线程的加锁计数器
private transient HoldCounter cachedHoldCounter;
// 首个读锁持有者
private transient Thread firstReader = null;
// 首个读锁持有者的加锁次数
private transient int firstReaderHoldCount;
// 某个线程的加锁次数计数器
static final class HoldCounter {
int count = 0;
// Use id, not reference, to avoid garbage retention
final long tid = getThreadId(Thread.currentThread());
}
// 用于存储加锁计数器的 TheadLocal
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
}
1.1.记录加锁次数
// 共享模式的位移量,即 16 位后是写锁,其及之前为写锁
static final int SHARED_SHIFT = 16;
// 1 无符号右移 16 为,即用在共享锁上的 1
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
// 此为独占锁的掩码,用于与 state 的高 16 为进行与运算,从而获得写锁次数
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
// 获取共享锁(读锁)加锁次数
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
// 获取独占锁(写锁)加锁次数
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
// 同步状态 & 锁状态,来自 AQS 父类
private volatile int state;
由于 AQS 只有一个 state,因此在 Sync 中需要将 state 一个掰成两个用——它将 32 位的 int 值一分为二,高 16 位记录读锁,低 16 位记录写锁,因此这里放了四个常量:
SHARED_SHIFT:这个常量定义了共享模式的位移量,它指示了state中用于记录共享锁(读锁)次数的位数。在这里,共享锁的次数占据了高16位。SHARED_UNIT:这个常量定义了共享模式下每个共享锁的计数值,即共享锁的+ 1中的 1。它使用位移运算 将 1 左移16位,即1 << SHARED_SHIFT,得到的值为 2^16,即 65536,即每当获取一次读锁,就会让state + 65536。MAX_COUNT:这个常量定义了共享模式下最大的锁计数值。它使用位移运算和减法计算得到,即(1 << SHARED_SHIFT) - 1,等同于SHARED_UNIT - 1,也就是 65535。EXCLUSIVE_MASK:这个常量定义了用于获取独占锁(写锁)计数的位掩码,即低 16 位。
有了上述常量,我们就可以通过修改或获取读写锁的各自的次数:
- 计算读锁次数:
sharedCount(int c)方法,它使用无符号右移运算符>>>将state的高16位移动到低16位,然后返回结果; - 递增/递减读锁次数:直接对
state加/减SHARED_UNIT; - 获取写锁次数:
exclusiveCount(int c)方法,它使用位与运算符&和EXCLUSIVE_MASK进行位掩码操作,提取state的低16位,即独占锁的计数值; - 递增/递减写锁次数:直接对
state加/减1;
1.2.记录线程加锁状态
// 读锁计数器
private transient ThreadLocalHoldCounter readHolds;
// 当前线程的加锁计数器
private transient HoldCounter cachedHoldCounter;
// 首个读锁持有者
private transient Thread firstReader = null;
// 首个读锁持有者的加锁次数
private transient int firstReaderHoldCount;
// 某个线程的加锁次数计数器
static final class HoldCounter {
int count = 0;
// Use id, not reference, to avoid garbage retention
final long tid = getThreadId(Thread.currentThread());
}
// 用于存储加锁计数器的 TheadLocal
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
除了巧妙的使用 state 记录当前锁的整体读写锁获取次数外,Sync 还提供了一些其他的属性用于记录和管理不同线程的对锁的操作情况;
readHolds:这是一个用于记录读锁计数的ThreadLocalHoldCounter实例。每个线程在获取读锁时,会在readHolds中记录自己的读锁计数。它的作用是跟踪每个线程持有的读锁数量。cachedHoldCounter:这是一个用于存储当前线程的加锁计数的HoldCounter实例。它用于缓存当前线程的加锁计数,以避免频繁地创建和销毁HoldCounter对象。firstReader:这是一个引用,指向第一个获取读锁的线程。它用于记录读锁队列中的第一个读锁持有者,以便在释放读锁时进行优化操作。firstReaderHoldCount:这是首个读锁持有者的加锁次数。它用于记录第一个获取读锁的线程的读锁计数,以便在释放读锁时进行相应的减少。HoldCounter:这是一个用于记录线程加锁次数的内部类。每个线程在获取锁时,会在HoldCounter中记录自己的加锁次数。ThreadLocalHoldCounter:这是一个继承自ThreadLocal的内部类,用于存储加锁计数器HoldCounter的实例。每个线程在获取锁时,会通过ThreadLocalHoldCounter获取自己的HoldCounter实例,并在其中记录加锁次数。
对于上述成员变量,我们不必立刻理解它们,现在有个印象即可。
2.读锁的加锁过程
共享锁中的读锁或者写锁,主要的区别在于调用 AQS 中的 acquireShared 方法还是 acquire 方法。
2.1.读锁的加锁流程
在内部类 ReadLock 中,lock 方法最终调用了其持有的 Sync 实例的 acquireShared 方法:
public void lock() {
// 该方法来自 AQS
sync.acquireShared(1);
}
public final void acquireShared(int arg) {
// 先尝试加锁
if (tryAcquireShared(arg) < 0)
// 失败了就去排队
doAcquireShared(arg);
}
2.1.1.第一次尝试加锁
和独占锁 ReentrantLock 调用的 acquire 一样,同样先尝试加锁,等到成功了就读锁 + 1 :
// 这个方法只要返回了 -1,那么当前线程就要去等待队列排队
protected final int tryAcquireShared(int unused) {
// 获取当前线程
Thread current = Thread.currentThread();
int c = getState();
// 1、如果存在写锁,且不被自己持有,那么直接去排队
// 注意,换句话说,如果存在写锁,但是被自己持有,那么就允许继续获得读锁
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
// 获取读锁次数
int r = sharedCount(c);
// 2、根据公平性判断是否当前线程是否可以直接获得锁,且读锁数量未到最大值,且尝试 CAS 让写锁 + 1 成功
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
// 1.1 当前是首个加读锁的线程
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
}
// 1.2 当前线程已经获得过一次读锁,直接递增
else if (firstReader == current) {
firstReaderHoldCount++;
}
// 1.3 当前线程不是首个获取读锁的线程
else {
// 把当前的读锁计数器换成自己的,然后 + 1
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++; // 读锁计数器 + 1
}
return 1;
}
// 3、由于公平性限制读锁无法重入,或者读锁已经达到最大数量,又或者读锁 CAS 加锁失败
return fullTryAcquireShared(current);
}
上述方法大体分为两个逻辑:
- 判断是否存在写锁,如果有写锁,则线程直接进入等待队列,实现读写互斥; 不过,如果写锁被自己持有,那么是不影响获取读锁的,换言之,就是向下兼容;
- 尝试获取读锁,如果不违反公平性原则,则尝试直接获取读锁,并将自己的读锁计数器设置为
cachedHoldCounter。即使其他线程已经获取了读锁,也不会影响这个过程的进行,实现读读不互斥。
如果上述两种情况都不符合,则进入 fullTryAcquireShared 进行进一步处理。
2.1.2.锁降级
尤其需要注意的是,如果不进入等待队列,实际上就不会去唤醒后继节点,结合获取写锁以后还能继续获取读锁,这里实际上就不难理解所谓“锁降级”了:
- 线程先获取的写锁,并在写锁未释放前就再尝试获取的读锁;
- 由于写锁的存在,其他的线程必然不可能获取读锁,因此该线程几乎必然能够顺利的获取读锁;
- 此时线程同时成功持有写锁和读锁;
- 当写锁释放后,第一个持有读锁的还是这个线程,避免了后续的节点的“插队”问题;
2.2.读锁的第二次加锁
fullTryAcquireShared 是一个比较长的逻辑,在三种情况下线程会进入这个方法:
- 由于
readerShouldBlock方法的公平性限制,已经获得读锁的线程无法重入; - 由于读锁次数已经到达最大值;
- 由于 CAS 对读锁 + 1 失败。
显然这三种情况严格来说都不是正常情况,因此到这一步以后就需要在循环中反复进行,直到成功或者因为其他原因失败为止。
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null; // 当前线程的读锁计数器
// 注意,与独占锁不同,共享锁在这个时候就开始 CAS 了,而独占锁需要进入等待队列后才会开始 CAS
for (;;) {
int c = getState();
// 1、如果当前存在写锁,且写锁不被当前线程持有,那直接去排队
// 同样的,如果写锁被自己持有,那么照样可以拿读锁
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
// else we hold the exclusive lock; blocking here
// would cause deadlock.
}
// 2、如果当前不存在写锁,但是因为公平性导致不允许去抢读锁
else if (readerShouldBlock()) {
// firstReader 就是当前线程,说明这次是重入了,那么直接重新走一遍获取锁的逻辑
// Make sure we're not acquiring read lock reentrantly
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
} else {
// 当前线程不是重入线程,看看他的读锁计数器是否大于0,不是说明没有它不再持有读锁,直接去排队
if (rh == null) {
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) {
rh = readHolds.get();
if (rh.count == 0)
readHolds.remove();
}
}
if (rh.count == 0)
return -1;
}
}
// 3、读锁已经达到最大值,直接报错
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 4、尝试 CAS 让读锁 + 1
if (compareAndSetState(c, c + SHARED_UNIT)) {
if (sharedCount(c) == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
if (rh == null)
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
cachedHoldCounter = rh; // cache for release
}
return 1;
}
}
}
fullTryAcquireShared 相当于一个更详细的 tryAcquireShared。在这个方法中,当前线程会反复尝试使用 CAS 来让写锁的计数器加 1,直到出现以下情况之一:
- 写锁计数器加 1,当前线程成功获取写锁;
- 写锁已经被其他线程占用,当前线程需要进入等待队列排队;
- 当前线程因为公平性原则而无法获取写锁,且该线程之前并没有获得过任何写锁,此时也需要进入等待队列排队。
除此之外,我们现在大概能够理解 cachedHoldCounter 的作用。它实际上是用来记录当前获取读锁的线程的锁计数器。与独占锁的 exclusiveOwnerThread 不同,由于在共享模式下加锁后会立即唤醒下一个线程,因此 cachedHoldCounter 指针会在短时间内被多个获取读锁的线程连续替换,以指向自己的计数器。
从这个角度来看,它有点类似于独占锁中的 exclusiveOwnerThread,但它只属于读锁线程。
2.3.加入等待队列排队等锁
acquireShared 走完以后,返回值可能有两种:
- -1:获取写锁失败,当前线程需要去排队;
- 1:获取读锁成功,当前线程直接释放;
如果返回值为 -1,则会进入 doAcquireShared 方法:
private void doAcquireShared(int arg) {
// 以共享模式加入等待队列
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 获取前驱节点,若果前驱节点已经是头结点,说明当前节点在等待队列的最前端,即可以直接去获取锁
final Node p = node.predecessor();
if (p == head) {
// 再次尝试获取读锁,此处结果依然可能为 -1 或者 1
int r = tryAcquireShared(arg);
if (r >= 0) {
// 如果获取成功了,直接将当前节点设置为头结点,并且唤醒后继节点
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
// 清理前队列中的无效节点,并将最接近的正常前驱节点状态设置为 SIGNAL,然后阻塞自己
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
基本逻辑与独占锁一致,主要的不同在于在循环中通过 tryAcquireShared 方法加的是读锁,且在加锁会调用 setHeadAndPropagate 尝试唤醒等待队列中的后继节点:
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
// 将当前节点设置为头结点
setHead(node);
// 从 doAcquireShared 进入该方法 propagate 必然为 1,因此必然满足条件
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
// 获取当前节点(头结点)的后继节点,即等待队列的首个节点
Node s = node.next;
if (s == null || s.isShared()) // 如果后继节点是共享节点
// 释放所有后继节点,
// 注意这里相当于获取锁成功后立刻唤醒下一线程
doReleaseShared();
}
}
而如果在 for 循环中确加锁失败了,那就会在 parkAndCheckInterrupt 这一步直接阻塞,直到上一个节点调用了 release 唤醒当前节点为止。
2.4.唤醒后继节点
在 setHeadAndPropagate 中会把当前节点设置为头结点,即实际意义上的让当前节点从等待队列出队,然后再调用 doReleaseShared 唤醒等待队列中下一个节点:
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
// 若该节点状态为 SIGNAL,则表示当前节点需要被唤醒
// 尝试设置其等待状态为 0 (表示不需要唤醒),
// 如果 CAS 操作成功,则使用 unparkSuccessor 方法唤醒当前节点的后继节点
// 如果 CAS 操作失败,那就再来一次
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
// 如果等待状态为0,表示当前节点不需要被唤醒,但需要将状态设置为 PROPAGATE,
// 以确保这个节点在被释放后可以继续传播通知,去释放后续的节点
// 成功了就结束了,失败了就继续,直到成功为止
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
当走完 doReleaseShared 方法后,必然进入这两种分支的任意一者:
- 当前节点状态为
SIGNAL,正常的唤醒的后继节点; - 当前节点状态为
PROPAGATE,未能正常的唤醒后继节点;
在这里我们可以注意到,共享模式下加锁与独占模式下加锁的最大不同:读锁加锁成功后会立刻唤醒后继节点,而不是使其阻塞直到当前获取锁的线程释放锁。
3.读锁的解锁过程
在内部类 ReadLock 中,unlock 方法最终调用了其持有的 Sync 实例的 releaseShared方法:
public void unlock() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
doReleasShared 之前已经分析过了,即通过在循环中 CAS 去设置前驱节点的状态为 SIGNAL ,然后唤醒后继节点。因此这里我们先关注 tryReleaseShared :
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
// 是首个获取读锁的线程,说明此时没其他线程跟它竞争
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else {
// 如果不是首个获取读锁线程,则将cachedHoldCounter指向自己的计数器,表明读锁被自己占有
// 并按递减当前线程的读锁次数
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
readHolds.remove(); // 计数器置为0,从ThreadLocal移除计数器
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
// cas 递减全局的读锁次数,一直成功为止
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;
}
}
注意,当 doReleasShared 指向完毕后,返回的返回值有两种情况:
true:当此处读锁释放后,state等于 0,相当于实际上当前已经没有任何线程持有读锁或者写锁(废话);false:当此处读锁释放后,仍然还有其他的线程持有读锁;
当 doReleasShared 返回 true 时,才会唤醒队列中的后继节点。
4.写锁的加锁过程
4.1.加锁过程
在内部类 WriteLock 中,lock 方法最终调用了其持有的 Sync 实例的 acquire方法,这一步就比较乏善可陈了,因为它实际上就是一个标准的独占锁加锁流程:
public void lock() {
sync.acquire(1);
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
其中,tryAcquire 依然来自于ReentrantReadWriteLock 的 Sync 内部类:
protected final boolean tryAcquire(int acquires) {
/*
* Walkthrough:
* 1. If read count nonzero or write count nonzero
* and owner is a different thread, fail.
* 2. If count would saturate, fail. (This can only
* happen if count is already nonzero.)
* 3. Otherwise, this thread is eligible for lock if
* it is either a reentrant acquire or
* queue policy allows it. If so, update state
* and set owner.
*/
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c); // 获取写锁次数
// 有写锁或者读锁
if (c != 0) {
// 写锁为0,说明已经有读锁了,加锁失败
// 写锁不为0,但是被其他线程持有,加锁失败
// (Note: if c != 0 and w == 0 then shared count != 0)
if (w == 0 || current != getExclusiveOwnerThread())
return false;
// 如果写锁超过最大次数就异常
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
// 直接 cas 让写锁 + 1
setState(c + acquires);
return true;
}
// 没有写锁或者读锁
// 先进行公平性检查,看能不能直接拿锁,不能就直接加锁失败
// cas 让写锁 + 1
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
相比于 ReentrantLock 的加锁逻辑,ReentrantReadWriteLock 在加锁前多了一步判断,即确认是否存在读锁,如果存在读锁则直接加锁失败,这反映了两个问题:
- 读写锁是互斥的;
- 若当前线程持有读锁,在未释放前不允许直接加写锁,即不支持锁升级;
如果在这一步未能直接加锁成功,那么就又回到了我们熟悉的入队等锁环节:
addWaiter(Node.EXCLUSIVE):把线程封装为Node节点并加入等待队列;acquireQueued:在等待队列中不断尝试 CAS 获取写锁,每次失败都会调用LockSupport.park直接挂起,并等待前驱节点的唤醒;
4.2.潜在的死锁问题
上述代码揭示了一种可能导致死锁的场景:
- 线程先获取读锁,又尝试获取写锁;
- 线程在
tryAcquire方法中发现已经存在读锁,因此直接进入等待队列,在acquireQueued循环中尝试使用 CAS 获取写锁; - 当在循环中获取写锁失败后,线程会调用
LockSuppor.park把自己挂起; - 此时读锁线程可能被前驱节点调用
LockSuppor.unpark唤醒,这种情况下线程刚获取读锁就又被挂起了; - 由于读锁线程被挂起,因此读锁不可能被释放,导致写锁也不可能被释放;
- 线程进入死锁状态,GG;
因此,我们在使用时需要保证:在加写锁前,读锁必须先释放。
5.写锁的解锁过程
在 WriteLock 内部类中,lock 方法最终调用了其持有的 Sync 实例的 release方法。这一步就像一个标准的独占锁解锁流程一样简单。
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
接下来,代码会调用 ReentrantReadWriteLock 的 Sync 内部类的 tryRelease 方法:
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;
}
总结
读写锁的数据结构
- 锁状态:它将 AQS 中的 32 位的
state变量一分为而,其中高 16 用于记录读锁的加锁次数,低 16 位用于记录写锁的加锁次数,通过位运算获取和操作锁状态;- 读锁加 1 :将 1 无符号左移 16 为以后,即为读锁的
1; - 写锁加 1 :直接加
1即可; - 是否有锁:只要
state不为0,即认为可能存在读锁或者写锁;
- 读锁加 1 :将 1 无符号左移 16 为以后,即为读锁的
- 持锁线程管理:在读写锁中,独占锁的标志位
exclusiveOwnerThread用于记录持有写锁的线程,而读锁线程的管理由其他变量处理:cachedHoldCounter:是一个HoldCounter类型的变量,表示最近一次成功抢占读书的线程的锁计数器,在其他线程成功获取锁后会被替换;readHolds:用于记录所有获取了读锁的线程的HoldCounter,当线程首次获取读锁后会进行初始化;
读锁的加解锁
加锁
- 调用读锁的
lock方法后,最终会调用 AQS 的tryAcquireShared方法; - 在
tryAcquireShared方法中,将会先判断是否存在不被当前线程持有的写锁,有的话直接加锁失败进入等待队列; - 如果不存在写锁,或者写锁被自己持有(即已经获取写锁的情况下,可以继续获得读锁),那么就尝试加读锁;
- 在这个过程中,将循环会尝试把当前线程的
HoldCounter放到cachedHoldCounter上(如果不存在,就先创建一个,然后放到readHolds缓存),然后让state的读锁部分和HoldCounter计数加1; - 如果不成功,说明可能在循环过程出现的写锁,那么就调用
doAcquireShared进入等待队列排队等锁; - 在
doAcquireShared中,线程同样会循环的等锁,如果成功了,就会让state的读锁部分和HoldCounter计数加1,然后唤醒后继节点; - 如果失败了,就会调用
LockSupport.park把自己挂起,直到被前驱节点唤醒;
解锁
- 调用读锁的
unlock方法后releaseShared方法; - 在循环中尝试让
cachedHoldCounter指向的计数器改为自己的HoldCounter,然后让其与state中的读锁部分减一,如果HoldCounter已经减为0,就将其从readHolds缓存中移除; - 如果此时等待队列中还有等待的线程,那么就将其唤醒;
写锁的加解锁
加锁
- 调用写锁的
lock方法后,将会调用acquire方法进行加锁; - 它依然会先调用
tryAcquire尝试进行一次加锁,在这个过程中:- 如果有读锁,不论是不是自己的读锁,都直接加锁失败;
- 如果有写锁,且不是自己的,那么加锁失败,进入等待队列;
- 如果没有任何锁,或者有写锁但是是自己的,那么就尝试 CAS 让
state的写锁部分加一,成功就设置独占标志位,失败就去等待队列;
- 当
tryAcquire失败后,先调用addWaiter进入等待队列,然后调用acquireQueued在队列中循环尝试加锁,每失败一次就调用LockSupport.park把自己挂起,等到前驱节点唤醒后再重复该过程; - 重复上述过程,直到成功加锁为止;
解锁
- 调用 AQS 的
release方法,该方法最终会调用tryRelease方法; - 在
tryRelease方法中,将会检查当前持有写锁的是否为当前线程,不是直接报错,若是则对state写锁部分减1,并移除独占标志位; - 若有后继节点,则会将其唤醒;
锁升级与锁降级
锁降级
一般来说,锁降级是指当一个线程获取了写锁后,再尝试获取读锁,此时哪怕之前有其他线程在排队,该线程也必然能够“插队”获得读锁。 之所以有这个情况,是因为:
- 线程先获取的写锁,并在写锁未释放前就再尝试获取的读锁;
- 在获取读锁的
tryAcquireShared反复法中,判断若写锁被当前线程持有,那么不影响它继续获取读锁; - 并且,由于此时写锁的存在,其他的线程必然不可能成功获取读锁,因此该线程几乎必然能够顺利的获取读锁;
这个操作某种程度上可以认为是读写锁间的“重入”,它避免的在线程已经持有写锁的情况下,再获取读锁还需要再进行竞争和等待的问题。
锁升级
锁升级一般与锁降级对应,指当一个线程获取读锁后,可以直接获取写锁而不需要排队,这是做不到的。 写锁不同,当获取了写锁后,所有后继节点都被挂起,因此加读锁的时候不竞争而直接“插队”。而对于读锁,在同一时间,可能有多个获取了读锁的线程处于运行状态,这种情况下想要无竞争的让一个线程“走后门”拿到写锁是不可能的。 另外,由于写锁线程需要等待读锁线程,当获取读写锁的现场都是同一个的时候,还可能会引发死锁:
- 线程先获取读锁,又尝试获取写锁;
- 线程在
tryAcquire方法中发现已经存在读锁,因此直接进入等待队列,在acquireQueued循环中尝试使用 CAS 获取写锁; - 当在循环中获取写锁失败后,线程会调用
LockSuppor.park把自己挂起; - 此时读锁线程可能被前驱节点调用
LockSuppor.unpark唤醒,这种情况下线程刚获取读锁就又被挂起了; - 由于读锁线程被挂起,因此读锁不可能被释放,导致写锁也不可能被释放;