独占式同步状态的获取和释放
1. 获取
代码块1-1
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
通过acquire()获取同步状态,关注tryAcquire(arg) 和
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)的执行,此处分两种情况讨论。
- 如果成功获取,则 tryAcquire(arg)返回true,acquire(int arg)直接返回,流程结束。
- 如果不成功,执行 acquireQueued(addWaiter(Node.EXCLUSIVE), arg),接下来分析这两个方法。
1.1 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)这句代码实际上分两步执行:
Node node =addWaiter(Node.EXCLUSIVE);
acquireQueued(node, arg);
接下来依次分析这两个方法。
1.1.1 Node node =addWaiter(Node.EXCLUSIVE);
先分析addWaiter(Node.EXCLUSIVE),其源码如下:
代码块1-2
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
addWaiter(Node.EXCLUSIVE)做了这几件事:
- 基于当前线程,创建一个同步队列的节点node,模式设为独占模式。
- 尝试将该节点插入到同步队列的队尾,可能成功可能失败,如果插入成功就直接返回当前节点,如果插入失败,进入第三步。
- 插入失败了,调用 enq(node),再次尝试插入,下面分析该方法。
代码块1-3
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
在 enq(final Node node)方法中,通过死循环来保证节点的正确添加。只有通过CAS将节点设置成尾节点后,当前线程才能从该方法返回。
对 addWaiter(Node mode)的总结就是:在获取同步状态失败后,保证当前线程对应的节点能成功插入到同步队列的尾部,然后将该节点返回。
1.1.2 acquireQueued(node, arg);
代码块1-4
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
在addWaiter(Node mode)返回当前线程的节点后,开始执行acquireQueued(final Node node, int arg) ,现在开始分析该方法,这个方法做了这几件事:
- (1)如果当前线程的节点的先驱节点是头结点,(2)那当前线程还能在尝试获取一下同步状态。如果(1)(2)都成功,则当前线程获得同步状态,将当前线程的节点设为同步队列的头结点,原来的头结点从同步队列中移除。如果(1)或(2)有一个失败了,则进入第2步。
- 执行if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())中的逻辑,下面分析这两个方法。
(1)对:shouldParkAfterFailedAcquire(p, node) (参考:https://blog.csdn.net/anlian523/article/details/106448512/的shouldParkAfterFailedAcquire源码分析的部分)
简单来说就是:把node的有效前驱(有效是指node不是CANCELLED的)找到,并且将有效前驱的状态设置为SIGNAL,之后便返回true代表马上可以阻塞了。
(2)如果(1)返回true,则执行parkAndCheckInterrupt(),在该方法里调用park()。此时,当前线程终于暂停执行了,进入waiting状态,代码块如下。
代码块1-5
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
在此状态下,有两种途径可以唤醒该线程:
1)被unpark();
2)被interrupt()。
至此,acquireQueued(node, arg);也分析完了。总结如下:
- 当前线程会在死循环中不断尝试获得同步状态。
- 如果同步状态获取失败,就会不断检查当前线程的节点的先驱节点的状态,当先驱节点的状态为SIGNAL,当前线程暂停,进入waiting状态。
- 在waiting状态时,线程等待unpark()或interrupt()唤醒自己,唤醒后重复上面的步骤,直到成功获取同步状态,或再一次暂停。
1.2 总结
独占获取的大概流程总结如下:
- 首先调用自定义同步器实现的tryAcquire(int arg),该方法保证线程安全的获取同步状态。
- 如果同步状态获取失败,则构造同步节点,节点模式为独占,通过addWaiter(Node node)将该节点加入到同步队列的尾部。
- 最后调用acquireQueued(Node node ,int arg),使得该节点以死循环的方式获取同步状态。如果获取不到,则节点对应的线程进入等待状态,而暂停线程的唤醒主要依靠先驱节点的出队或阻塞线程被中断来实现。
简化的流程图如下:
2.释放
代码块1-6
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
- 首先执行tryRelease方法(该方法由子类实现),如果成功,则执行if条件语句里面的代码,唤醒下一个后继结点。
- 获得头结点,然后如果它不空且它的状态不为0的话(关于状态的解释:结点的初始化状态为0,不为0的状态在独占模式下要么为CANCEL和SINGNAL,CANCEL的话这种状态的结点应该是在acquire的时候给剔除了,因为shouldParkAfterFailedAcquire这个方法会判断当前结点的前驱结点,然后符合规则的置waitstatus为SINGNAL,不符合的则继续往前找,直到找到之后将该前驱结点的next指向当前结点,则中间的结点都会断链,然后被GC。SINGNAL状态代表着该结点的后续结点需要被唤醒),就调用unparkSuccessor方法。
- 在unparkSuccessor(Node node)里,(1)将当前节点状态通过CAS置为0,(2)从队列的最后面往前去找离当前结点最近的状态小于等于0的结点,然后去唤醒它。
参考:
https://blog.csdn.net/a6822342/article/details/84839391