public void lock() {
sync.acquireShared(1);
}
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
(一)、tryAcquireShared过程
-
如果当前锁写状态不为0且占锁线程非当前线程,那么返回占锁失败的值-1。
-
如果公平策略没有要求阻塞且重入数没有到达最大值,则直接尝试cas更新state。
-
如果cas操作成功,有以下操作逻辑:
-
首先,如果当前读锁计数为0那么就设置第一个读线程就是当前线程。
-
其次,当前线程和firstReader同一个线程,记录firstReaderHoldCount也就是第一个读线程读锁定次数。
-
最后,读锁数量不为0并且不为当前线程,获取当前线程ThreadLocal当中的读锁重入计数器。
-
结果返回占锁成功的值1
-
如果cas操作失败,有以下操作逻辑:
-
通过fullTryAcquireShared尝试获取读锁,内部处理和tryAcquireShared过程相同。
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
// 如果当前锁写状态不为0且占锁线程非当前线程,那么返回占锁失败。
// 也就是当前线程先占写锁后可以再占读锁的,反之不行。
if (exclusiveCount© != 0 &&
getExclusiveOwnerThread() != current)
return -1;
// 判断高位的读状态标记
int r = sharedCount©;
//如果公平策略没有要求阻塞且重入数没有到达最大值,则直接尝试CAS更新state
//如果读不应该阻塞并且读锁的个数小于最大值65535,并且可以成功更新状态值
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
// 如果当前线程满足对state的操作条件,
// 就利用CAS设置state+SHARED_UNIT,实际上就是读状态+1。
// 但是需要注意,这个state是全局的,即所有线程获取读锁次数的总和,
// 而为了方便计算本线程的读锁次数以及释放掉锁,
// 需要在ThreadLocal中维护一个变量。这就是HoldCounter。
//如果当前读锁为0
if (r == 0) {
// 第一个读线程就是当前线程
firstReader = current;
firstReaderHoldCount = 1;
}
//如果当前线程重入了,记录firstReaderHoldCount
else if (firstReader == current) {
firstReaderHoldCount++;
}
//当前读线程和第一个读线程不同,记录每一个线程读的次数
else {
// 每个线程自己维护cachedHoldCounter
HoldCounter rh = cachedHoldCounter;
// 计数器为空或者计数器的tid不为当前正在运行的线程的tid
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
//用来处理CAS没成功的情况,逻辑和上面的逻辑是类似的,就是加了无限循环
return fullTryAcquireShared(current);
}
(二)、fullTryAcquireShared过程
-
fullTryAcquireShared内部通过for()循环进行逻辑操作。
-
内部处理和tryAcquireShared过程相同。
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
// 无限循环
for (;😉 {
// 获取状态
int c = getState();
// 写线程数量不为0且当前线程不是写线程那么返回获锁失败
if (exclusiveCount© != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
}
// 写线程数量为0并且读线程被阻塞
else if (readerShouldBlock()) {
if (firstReader == current) {
// 当前线程为第一个读线程
// assert firstReaderHoldCount > 0;
} else {
// 当前线程不为第一个读线程
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;
}
}
// 读锁数量为最大值,抛出异常
if (sharedCount© == MAX_COUNT)
throw new Error(“Maximum lock count exceeded”);
// 比较并且设置成功,后续的这部分逻辑跟之前讲的一模一样
if (compareAndSetState(c, c + SHARED_UNIT)) {
if (sharedCount© == 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;
}
}
}
(三)、doAcquireShared过程
-
doAcquireShared主要实现获读锁失败后的等待操作。
-
doAcquireShared通过addWaiter(Node.SHARED)将当前线程封装成SHARED类型Node并添加到CLH队列。
-
如果当前线程的Node节点是CLH队列的第一个节点则当前线程直接获取锁并开启读锁的扩散唤醒所有阻塞读锁的线程。
-
如果当前线程的Node节点不是CLH队列的第一个节点那么就通过parkAndCheckInterrupt进入休眠。
-
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();
// 如果前一个节点是head节点(head节点不保存任何线程),
// 表明当前节点是第一个等待唤醒节点
if (p == head) {
// 尝试获取锁
int r = tryAcquireShared(arg);
// 如果获锁成功
if (r >= 0) {
// 说明当前线程获取读锁成功,那么设置当前线程Node为head
// 同时扩散唤醒相关读线程,因为读线程之间相互不阻塞,可以一起唤醒继续工作
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
// 如果“当前线程”不是CLH队列的表头,
// 则通过shouldParkAfterFailedAcquire()判断是否需要等待,
// 需要的话,则通过parkAndCheckInterrupt()进行阻塞等待。
// 若阻塞等待过程中,线程被中断过,则设置interrupted为true。
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private Node addWaiter(Node mode) {
// 为当前线程和给定模式创建和排队节点
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
(四)、setHeadAndPropagate过程
-
设置当前线程的Node为CLH队列的head节点。
-
判断当前节点的后置节点为空或者是SHARED状态那么就唤起后置的读锁阻塞线程。
-
doReleaseShared在解锁过程也同样提及,放到后面解释。
private void setHeadAndPropagate(Node node, int propagate) {
// 设置当前节点为新的head节点
Node h = head;
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();
}
}
一线互联网大厂Java核心面试题库
正逢面试跳槽季,给大家整理了大厂问到的一些面试真题,由于文章长度限制,只给大家展示了部分题目,更多Java基础、异常、集合、并发编程、JVM、Spring全家桶、MyBatis、Redis、数据库、中间件MQ、Dubbo、Linux、Tomcat、ZooKeeper、Netty等等已整理上传,感兴趣的朋友可以看看支持一波!
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
// 当前节点的后置节点
Node s = node.next;
if (s == null || s.isShared())
// 唤醒后起等待线程
doReleaseShared();
}
}
一线互联网大厂Java核心面试题库
[外链图片转存中…(img-3o4qtvfF-1714753397896)]
正逢面试跳槽季,给大家整理了大厂问到的一些面试真题,由于文章长度限制,只给大家展示了部分题目,更多Java基础、异常、集合、并发编程、JVM、Spring全家桶、MyBatis、Redis、数据库、中间件MQ、Dubbo、Linux、Tomcat、ZooKeeper、Netty等等已整理上传,感兴趣的朋友可以看看支持一波!