AQS共享资源
2022-03-25 本文已影响0人
程序员札记
共享模式获取资源( acquireShared(int i) )
在共享模式下线程获取资源的顶级入口,他会获取指定量的资源,如果全部释放 state=0了,那么他会唤醒等待队列的其他线程来获取资源。
通常执行流程是
- tryAcquireShared()尝试获取资源,成功则直接返回。
- 失败则通过doAcquireShared()进入等待队列,直到获取到资源为止才返回。
AQS返回值的语义定义:负值代表获取失败;0代表获取成功,但没有剩余资源;正数表示获取成功,还有剩余资源,其他线程还可以去获取。
public final void acquireShared(int arg) {
// 如果资源获取失败则放入队列
if (tryAcquireShared(arg) < 0)
// 共享方式放入等待队列
doAcquireShared(arg);
}
tryAcquireShared()
tryAcquireShared()
依然要自定义同步器去实现。和tryAcquire()
是一样的。
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) {
// 尝试获取资源当前节点的状态
int r = tryAcquireShared(arg);
// 如果当前资源状态还有资源可以获取
if (r >= 0) {
// 将头指向自己拿到资源线程此时node被唤醒,
// 也可能是head用完资源后来唤醒自己的.即自己需要的资源数量大于当前空闲资源能提供的数量。
setHeadAndPropagate(node, r);
//将前头节点的后继节点置空 让GC好回收
p.next = null; // help GC
// 判断该线程是否被中断过
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
// 执行过程和独享模式的执行过程相同
// 先寻找休息的位置 然后将线程pack了
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
setHeadAndPropagate()
共享模式下每一个拿到资源的线程在自己苏醒的情况下,如果条件符合比如还有剩余资源,都会唤醒后继的线程。如果被唤醒的线程发现资源不够用时会再次进入休眠。即便排在首位线程后面的线程只需要少量的资源也会因为首位线程资源不够造成的休眠而等待。
比如说 假设老大用完了然后释放了5个资源,老二这时需要7个资源、老三需要2个资源、老4需要2个资源。因为老大使用资源完成后优先唤醒的是排在队伍后面的老二但是老二发现当前资源不够用。就将自己park了,而排在老二之后的老三老四也不会被唤醒了,这样体量巨大的老二就把远小于自己的老三老四阻塞在队列之后,而在老大之前运行的线程释放资源以后才会再次唤醒老二,在这两次唤醒之间5个资源一直是处于无人访问状态的。
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head;
// 将头节点指向自己
setHead(node)
// 如果还有剩余的资源,继续唤醒下一个队列中的线程。
if (propagate > 0 || h == null || h.waitStatus < 0) {
// 设置队列中下一个元素
Node s = node.next;
if (s == null || s.isShared())
// 唤醒后继节点
doReleaseShared();
}
}
也就是说在自己线程苏醒的时候,会依据条件唤醒后继的节点。这就是共享模式区别于单机模式的精髓所在
释放共享资源releaseShared()
public final boolean releaseShared(int arg) {
// 尝试释放资源
if (tryReleaseShared(arg)) {
// 唤醒后继节点
doReleaseShared();
return true;
}
return false;
}
唤醒后继节点
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
// 如果该节点为等待唤醒的节点 则执行唤醒操作
if (ws == Node.SIGNAL) {
// 将H节点的数据从被等待唤醒转换为 初始化
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)){
continue;
}··
// 唤醒线程
unparkSuccessor(h);
}
// 如果该节点处于初始化状态 将其转换为可运行状态
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
// 如果
if (h == head)
break;
}
}
唤醒步骤:
- 进入自旋
- 如果头节点为空或者头节点不等于尾节点,则跳出自旋,说明等待队列已经空了,
- 判断当前头节点状态,如果是被标记为待唤醒状态的节点,初始化该节点。唤醒该节点的线程。
- 如果该节点是初始化状态则将其标记为可运行状态。当标记为PROPAGATE时会将唤醒流程传播下去。因为h == head不成立,就不会跳出循环。
总结
AQS源码中帮我们做好了线程排队、等待、唤醒等操作我们只需要重写决定如何获取和释放的锁。这是典型的模板方法。下一章我们讲讲AQS中其他好用的API
下面几篇我们将讲解下基于AQS同步框架的一些实现