selfInterrupt();
failed = false;
return;
}
}
/**
-
p不是头结点 or 获取锁失败,判断是否应该被阻塞
-
前继节点的ws = SIGNAL 时应该被阻塞
*/
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
node获取锁成功出队,设置新head,并将共享性传播给后继节点,即唤醒后继共享节点。为什么当一个节点的线程获取共享锁后,要唤醒后继共享节点?共享锁是可以多个线程共有的,当一个节点的线程获取共享锁后,必然要通知后继共享节点的线程,也可以获取锁了,这样就不会让其他等待的线程等很久,而传播性的目的也是尽快通知其他等待的线程尽快获取锁。
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
//设置node为新head
setHead(node);
/*
-
The conservatism in both of these checks may cause
-
unnecessary wake-ups, but only when there are multiple
-
racing acquires/releases, so most need signals now or soon
-
anyway.
*/
// propagate > 0,短路后面的判断
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
//唤醒后继共享节点
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
setHeadAndPropagate
中调用doReleaseShared
前需要一连串的条件判断,大概可以分为三部分:
1. propagate > 0
在ReentrantReadWriteLock
中走到setHeadAndPropagate
,只可能是propagate > 0
,所以后面判断旧、新head
的逻辑就被短路了。
而在Semaphore
中走到setHeadAndPropagate
,propagate
是可以等于0的,表示没有剩余资源了,故propagate > 0
不满足,往后判断。
2. h == null || h.waitStatus < 0
首先判断旧head是否为null
,一般情况下是不可能是等于null,除非旧head
刚好被gc
了。h == null
不满足,继续判断h.waitStatus < 0
,h.waitStatus
可能等于0,可能等于-3。
-
h.waitStatus=0
的情况,某个线程释放了锁(release or releaseShared
)或者前一个节点获取共享锁传播setHeadAndPropagate
,唤醒后继节点的时候将h.waitStatus=-1
设置为0。 -
h.waitStatus=-3
,doReleaseShared
唤醒head后继节点后h.waitStatus
从-1到0,还没来得及更新head,即被唤醒的共享节点还没有setHeadAndPropagate
,又有其他线程doReleaseShared
唤醒head后继节点h.waitStatus
从0到-3。
//java.util.concurrent.locks.AbstractQueuedSynchronizer#unparkSuccessor
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
//cas设置h.waitStatus -1 --> 0
compareAndSetWaitStatus(node, ws, 0);
//唤醒后继节点的线程,若为空or取消了,从tail往后遍历找到一个正常的节点
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
//uppark线程
LockSupport.unpark(s.thread);
}
当释放共享锁or共享锁传播后会调用doReleaseShared
唤醒同步队列中head的后继节点。
首先明确几个判断:
-
h.waitStatus = Node.SIGNAL
,compareAndSetWaitStatus(h, Node.SIGNAL, 0))
和unparkSuccessor
。 -
h.waitStatus = 0
,compareAndSetWaitStatus(h, 0, Node.PROPAGATE)
设置head为传播模式。 -
h == head
,head没有变,break中断循环;也可能被唤醒的节点立刻获取了锁出队列,导致head变了,所以继续循环唤醒head后继节点。
//java.util.concurrent.locks.AbstractQueuedSynchronizer#doReleaseShared
private void doReleaseShared() {
for (;😉 {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
//SIGNAL --> 0
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
//唤醒后继节点的线程
unparkSuccessor(h);
}
else if (ws == 0 &&
//0 --> PROPAGATE
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
/**
-
we must loop in case a new node is added
-
while we are doing this
*/
if (h == head) // loop if head changed
//head没有变则break
break;
}
}
3. (h = head) == null || h.waitStatus < 0
首先判断新head是否为空,一般情况下新head不为空,(h = head) == null
不满足,判断h.waitStatus < 0
,h.waitStatus
可能等于0,可能小于0(-3 or -1)。
-
h.waitStatus
可能等于0的情况,后继节点刚好入队列,还没有走到shouldParkAfterFailedAcquire()
中的修改前继节点waitStatus
的代码。 -
h.waitStatus=-3
,上一个共享节点被唤醒后,成为新head,后继节点刚入队列,又有其他线程释放锁调用doReleaseShared
,h.waitStatus
从0改为-3。 -
h.waitStatus=-1
,已经调用了shouldParkAfterFailedAcquire()
,h.waitStatus
从0 or -3 改为-1,可能阻塞,可能未阻塞。
//java.util.concurrent.locks.AbstractQueuedSynchronizer#shouldParkAfterFailedAcquire
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
-
This node has already set status asking a release
-
to signal it, so it can safely park.
-
node拿锁失败,前继节点的状态是SIGNAL,node节点可以放心的阻塞,
-
因为下次会被唤醒
*/
return true;
if (ws > 0) {
/*
-
Predecessor was cancelled. Skip over predecessors and
-
indicate retry.
-
pred节点被取消了,跳过pred
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/* 0 -3
-
waitStatus must be 0 or PROPAGATE. Indicate that we
-
need a signal, but don’t park yet. Caller will need to
-
retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
接下来通过控制变量法,求证共享锁的传播性。
对于ReentrantReadWriteLock
,既然走到了setHeadAndPropagate
,tryAcquireShared
返回值就一定大于0,即propagate > 0,所以对于ReentrantReadWriteLock
,PROPAGATE
只是一个中间状态值,即使没有PROPAGATE
也不会影响共享锁的传播性。
而Semaphore
具有资源的概念,走到setHeadAndPropagate
,tryAcquireShared
返回值代表资源剩余量,返回值可能等于0,所以需要配合状态PROPAGATE
完成共享锁的传播性。
接下来只讨论Semaphore
的传播情况。
(1)假设Semaphore
有10个资源都被占用。A线程获取资源失败入队列阻塞。此时C释放资源,唤醒A(h.ws=-1 ---> h.ws=0
)。A获取资源成功,但是没有资源了,tryAcquireShared
返回0(p=0
)。此时B线程获取资源失败进入队列,但是还没有阻塞(A.ws=0
)。
p>0
不满足,h==null || h.ws < 0
不满足,h=head==null || h.ws < 0
不满足。都不满足,A线程执行setHeadAndPropagate
不唤醒B线程,因为没有资源了,没必要唤醒。
(2)在(1)的基础上,当判断完3个条件后,D线程释放资源,B还没有阻塞,B也无需被D唤醒,B在阻塞前还有一次重试的机会;在D释放资源前,B已经阻塞了,就由D唤醒B。
(3)在(1)的基础上,当在判断3个条件前,B已经阻塞了,h=head==null || h.ws < 0
判断新head满足条件,A唤醒B,B可能因为没有资源而获取资源失败继续阻塞,造成了不必要的唤醒,也可能因为此时刚好有线程释放了资源,B获取资源出队列。
(4)在(1)的基础上,C唤醒A后,A获取锁成功,tryAcquireShared
返回0,A还未出队列,此时B又刚好获取锁失败进入队列。此时D释放资源h.ws=0 ---> h.ws=-3
。p>0不满足,h==null || h.ws < 0
满足,执行doReleaseShared
唤醒B。B还没有阻塞没必要唤醒,若B没有抢过其他线程而阻塞,此时唤醒B,B可能因为没有资源而获取资源失败继续阻塞,造成了不必要的唤醒,也可能因为此时刚好有线程释放了资源,B获取资源出队列。
(5)在(1)的基础上,C唤醒A后,A获取锁成功,tryAcquireShared
返回0,A刚出队列,还没执行到3个判断,B获取资源失败进入队列还未阻塞。此时D释放资源,新head h.ws=0 ---> h.ws=-3
,p>0
不满足,h==null || h.ws < 0
不满足,h=head==null || h.ws < 0
满足,执行doReleaseShared
唤醒B。B还没有阻塞没必要唤醒,若B没有抢过其他线程而阻塞,此时唤醒B,B可能因为没有资源而获取资源失败继续阻塞,造成了不必要的唤醒,也可能因为此时刚好有线程释放了资源,B获取资源出队列。
假设没有状态PROPAGATE
:
-
线程多次调用
doReleaseShared
,保持head的waitStatus
为0,(4)和(5)在B没有阻塞的前提是不会继续doReleaseShared
,若B线程在3个判断后阻塞,此时B就需要等下一个线程释放资源唤醒,这样有可能会导致B等待时间过长。 -
若多次
doReleaseShared
,head的waitStatus
改为-1,也是不合理的,这样可能会导致B还没有阻塞直接就判断应该阻塞,丧失了重试一次的机会。
-
ReentrantReadWriteLock
中PROPAGATE
只是一个中间状态,共享锁的传播性由setHeadAndPropagate
完成。 -
对于有资源概念的
Semaphore
,PROPAGATE
和setHeadAndPropagate
组合完成共享锁的传播性。 -
共享锁的传播性目的是尽快唤醒同步队列中等待的线程,使其尽快获取资源(锁),但是也有一定的副作用,可能会造成不必要的唤醒。
-
PROPAGATE
只设置给head的waitStatus
,让head节点具有传播性。 -
PROPAGATE
作为中间状态的流转(h.ws=0 ---> h.ws=-3 ---> h.ws=-1
)和临界判断(h.ws < 0
)。 -
出现
h.ws=0 ---> h.ws=-3
的情况:
-
有一线程获取共享锁后唤醒后继节点(
h.ws=-1--->h.ws=0
),这时有另一个线程释放了共享锁(h.ws=0--->h.ws=-3
)。(ReentrantReadWriteLock
和Semaphore
都可能有这种情况) -
有一线程释放了共享锁(
h.ws=-1--->h.ws=0
)又有一线程释放了共享锁(h.ws=0--->h.ws=-3
)。(Semaphore
可能有这种情况,ReentrantReadWriteLock
不可能,因为ReentrantReadWriteLock
不是每次释放共享锁都会唤醒head后继节点,必须完全释放锁)
- AQS的设计,尽快唤醒其他等待线程体现在3个地方:
-
共享锁的传播性。
-
doReleaseShared()
中head改变,会循环唤醒head的后继节点。 -
线程获取锁失败后入队列并不会立刻阻塞,而是判断是否应该阻塞
shouldParkAfterFailedAcquire
,如果前继是head,会再给一次机会获取锁。
参考:
https://zhuanlan.zhihu.com/p/112371628
https://www.zhihu.com/question/295925198?sort=created
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
![img](https://img-blog.csdnimg.cn/img_convert/b3cd97874cd5736e47af34c13bd81644.jpeg)
总结
就写到这了,也算是给这段时间的面试做一个总结,查漏补缺,祝自己好运吧,也希望正在求职或者打算跳槽的 程序员看到这个文章能有一点点帮助或收获,我就心满意足了。多思考,多问为什么。希望小伙伴们早点收到满意的offer! 越努力越幸运!
金九银十已经过了,就目前国内的面试模式来讲,在面试前积极的准备面试,复习整个 Java 知识体系将变得非常重要,可以很负责任的说一句,复习准备的是否充分,将直接影响你入职的成功率。但很多小伙伴却苦于没有合适的资料来回顾整个 Java 知识体系,或者有的小伙伴可能都不知道该从哪里开始复习。我偶然得到一份整理的资料,不论是从整个 Java 知识体系,还是从面试的角度来看,都是一份含技术量很高的资料。
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
给这段时间的面试做一个总结,查漏补缺,祝自己好运吧,也希望正在求职或者打算跳槽的 程序员看到这个文章能有一点点帮助或收获,我就心满意足了。多思考,多问为什么。希望小伙伴们早点收到满意的offer! 越努力越幸运!**
金九银十已经过了,就目前国内的面试模式来讲,在面试前积极的准备面试,复习整个 Java 知识体系将变得非常重要,可以很负责任的说一句,复习准备的是否充分,将直接影响你入职的成功率。但很多小伙伴却苦于没有合适的资料来回顾整个 Java 知识体系,或者有的小伙伴可能都不知道该从哪里开始复习。我偶然得到一份整理的资料,不论是从整个 Java 知识体系,还是从面试的角度来看,都是一份含技术量很高的资料。
[外链图片转存中…(img-Z3jrQ3fK-1713064252676)]
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!