文章目录
- 1 读锁解锁
- 1.1 tryReleaseShared()
- 1.2 doReleaseShared()
- 1.3 unparkSuccessor()
- 1.4 示意图
- 2 写锁解锁
- 2.1 tryRelease()
- 2.2 尝试解锁成功
- 2.3 setHeadAndPropagate()
- 5 后记
1 读锁解锁
查看下读锁的解锁相关源代码:
public void unlock() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
执行流程:调用读锁的unlock()方法
- 调用了同步器syc的释放读锁方法releaseShared()
- releaseShared()方法
- 首先尝试去释放读锁
- 成功 唤醒锁竞争队列中的结点
- 首先尝试去释放读锁
1.1 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 {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != 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
- 第一个加读锁记录置空,释放资源
- 不为1 ,执行计数-1
- 判断线程计数是否为1
-
否则代表是其他读锁线程
- 获取缓存的计数器
- 如果缓存的计数器为空或者不是当前线程计数器,重新获取
- 如果计数器计数<=1 ,移除当前线程计数器
- 如果是<=0,抛异常
- 计算器计数-1
-
第二步执行for循环
- 获取锁状态(计数)
- cas执行锁计数-1
- 失败,继续循环
- 成功,判断锁计数-1是否=0即当前锁未加锁
注:
- 如果当前有多个获取读锁的线程正在执行或者有读锁重入,那么某一个线程释放锁,锁计数不会为0,也就不会执行唤醒锁竞争队列结点的操作
1.2 doReleaseShared()
此时锁计数为0,即未加锁状态,执行唤醒操作,那么锁竞争队列如果有阻塞的结点,那么第二个一定是获取写锁的结点。
- 写锁获取共享,前面分析过加锁原理。锁竞争队列如果有阻塞的结点,那么第一个结点是哨兵结点。
源代码如下所示:
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
执行流程:
- 获取头结点(哨兵结点)
- 第一步判断头结点不为空且不等于尾结点
- 获取头结点状态
- 判断结点状态是否等于Node.SIGNAL(-1)
- cas尝试把头结点状态由-1置为0
- 失败重复下一次循环
- 成功唤醒头结点的后继节点
- 否则判断其他条件,这里放在注意事项里面
- 第二步判断此时如果h还是等于头结点,结束循环
注:
- 这个方法是继承自AbstractQueuedSynchronizer。上面第一步里面的否则判断分子,在ReentrantReadWriteLock里面不会用到,在其他同步器锁中使用。
1.3 unparkSuccessor()
这个方法也是继承自AbstractQueuedSynchronizer,在之前加锁和解锁-ReentrantLock详解-AQS-并发编程(Java)有讲解。读锁释放唤醒的是写锁结点,回到写锁park的地方继续执行,过程同ReentrantLock中一样这里不再赘述。
1.4 示意图
示意图如下1.4-1所示:
2 写锁解锁
我们在来看下写锁解锁流程:
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;
}
- 写锁解锁调用unlock()方法
- unlock()调用同步器的release()方法
- 尝试解锁
- 如果头结点不为空且等待状态不为0
- 唤醒后继结点
- 解锁成功
- 如果头结点不为空且等待状态不为0
- 解锁失败
2.1 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;
}
执行流程如下:
- 判断锁持有线程是不是当前线程
- 不是直接抛异常
- nextc:获取锁计数-1
- 判断读锁计数是否为0
- 为零,当前未加锁,锁持有线程置空
- 设置锁计数
- 返回锁计数是否为零的判断结果
- 为零尝试解锁成功
- 不为零尝试解锁未成功
2.2 尝试解锁成功
tryRelease()成功之后,锁竞争队列不为空的情况下唤醒后继结点。如果第二个结点是读锁线程结点,和上面读锁解锁的情况一样这里不再赘述。这里以写锁解锁,锁竞争队列第二个、第三个为读锁线程结点的情况,分析,开始解锁初始状态如下图2.2-1所示:
获取写锁的线程thread-0,执行unlock()方法,执行tryRelease()方法返回true,此时状态如下图所示:
程序继续执行,开始唤醒thread-1结点,执行unparkSuccessor()方法,该方法同上,unpark thread-1线程。
thread-1共享结点(即获取读锁时阻塞的),我们在回到读锁加锁阻塞的地方,即doAcquireShared()方法的parkAndCheckInterrupt()这里,源代码如下:
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) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
- unpark之后,线程tread-1继续for循环,获取对应结点的前驱即头结点。
- 执行tryAcquireShared()方法,thread-1获取读锁成功,返回1
- tryAcquireShared()在读锁加锁有分析,不在赘述
- setHeadAndPropagate()开始执行,会继续唤醒后继为读锁线程的结点。
注:
- 对比读锁加锁流程,doAcquireShared()方法在尝试获取读锁失败的时候执行,再次尝试获取读锁,在写锁没有释放的情况下,也会失败,不会执行if(r>=0)的语句块;这里是写锁释放之后,未加锁情况下,尝试获取读锁会成功,然后执行该语句块。
唤醒thread-1之后,状态如下图2.2-1所示:
2.3 setHeadAndPropagate()
设置头结点并唤醒后继的读锁线程结点,源代码如下:
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
执行流程如下:
- 设置新的头结点
- 条件判断,因为progagate参数为1判断条件为true
- 获取后继结点,判断s为空或者是共享结点
- 执行doReleaseShared(),继续唤醒后继结点
- 之前有分析,这里不再赘述
- 执行doReleaseShared(),继续唤醒后继结点
- 获取后继结点,判断s为空或者是共享结点
示例执行到这里,继续唤醒thread-2结点,效果如下2.3-1所示:
也就是说,在锁竞争队列不为空或者后继结点是读锁线程结点的情况下,会全部唤醒,这里也体现读锁共享的特征。示例到这里遇到写锁线程结点,暂时不唤醒。要等待线程执行完毕后在调用读锁解锁流程。
5 后记
如有问题,欢迎交流讨论。
❓QQ:806797785
⭐️源代码仓库地址:https://gitee.com/gaogzhen/concurrent
参考:
[1]黑马程序员.黑马程序员深入学习Java并发编程,JUC并发编程全套教程[CP/OL].2020-01-18/2022-12-12.p256~p258.