AQS核心流程解析cancelAcquire方法
作者:程序员李哈
引出问题
首先,先考虑一个问题,什么条件会触发cancelAcquire()方法?
cancelAcquire()方法的反向查找
可以清楚的看到在互斥锁和共享锁的拿锁过程中都是有调用此方法的,而cancelAcquire()方法是写在finally代码块中,并且使用failed标志位来控制cancelAcquire()方法的执行。可以得出,在触发异常的情况下会执行cancelAcquire()方法。
响应中断的获锁方法
可以清楚的看到,这里是响应异常,如果发生了异常,比如中断异常,那么当前线程Node需要做出取消的操作,那么下面详细的说明cancelAcquire()方法。
private void cancelAcquire(Node node) { // Ignore if node doesn't exist if (node == null) return; // 当前节点的线程指向置为null node.thread = null; // 协同取消的处理。 // 这里是判断当前节点的上一个节点的状态是否是取消状态(状态大于0只有是取消状态) // 如果上一个节点是取消状态,那么继续往上遍历,直到找到状态为小于0的状态节点。 // 并且把当前节点的prev指向非取消节点。 Node pred = node.prev; while (pred.waitStatus > 0) node.prev = pred = pred.prev; // 得到没有取消节点的下一个节点。 Node predNext = pred.next; // 因为当前cancelAcquire()方法就是取消的处理 // 所以将当前节点设置为取消状态。 node.waitStatus = Node.CANCELLED; // 如果当前取消的节点是tail节点,也就是最后一个节点 // 那么就把tail指针指向上面while循环遍历出的prev节点(因为要指向一个没有被取消的节点)。 if (node == tail && compareAndSetTail(node, pred)) { // help GC // 为什么说help GC呢? // 因为把prev的next节点设置为null, // 这样GC ROOT扫描发现没有根节点的引用。 compareAndSetNext(pred, predNext, null); } else { // 走到else代表当前节点不是tail节点,或者是cas操作的时候tail发生了变化 // 如果不是tail节点,不能直接把tail节点指向到上面while循环得出的prev节点 int ws; // 这里是的if代码块,是为了尝试一次,如果不成功再去复杂的处理。 // 这里的if判断条件如下: // 1.如果上面while循环得到的prev节点不是head节点 // 2.如果上面while循环得到的prev节点为-1,如果不为-1,cas改变成-1也。 // 3.如果上面while循环得到的rpev节点的线程指向不为null(如果为null代表在取消的过程中) // 因为&&是拼接,所以上面任意一个条件为false就会进入到else条件中。 if (pred != head && ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && pred.thread != null) { // 进到这里代表这次尝试成功了。 // 得到当前节点的下一个节点 // 然后把前面while循环得到的prev节点的next指向当前节点的next节点。 Node next = node.next; if (next != null && next.waitStatus <= 0) compareAndSetNext(pred, predNext, next); } else { // 直接唤醒当前节点的下一个节点。 // 唤醒的目的是为了去执行shouldParkAfterFailedAcquire方法去处理取消节点。 unparkSuccessor(node); } // 把当前节点的下一个节点指向自己. // help gc。 node.next = node; // help GC } }
比较复杂,所以笔者为了读者的观看顺利, 下面会拆分步骤,并且画图来理解。
跳过已经取消的节点,找到一个非取消的节点
// Skip cancelled predecessors // 跳过已经被取消的节点 Node pred = node.prev; while (pred.waitStatus > 0) node.prev = pred = pred.prev;
更新正常节点的链表
当前取消节点是tail节点的情况
if (node == tail && compareAndSetTail(node, pred)) { compareAndSetNext(pred, predNext, null); } else { // 后面讲 }
当前取消节点是非tail节点的情况
// 当前取消的节点不为tail节点的情况 int ws; // if的逻辑可以理解为尝试一次。 // 代表while循环得到的prev节点不是head节点 // 代表while循环得到的prev节点是可唤醒的正常节点 // 节点while循环得到的prev节点不是待取消节点 if (pred != head && ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && pred.thread != null) { Node next = node.next; // 当前节点的下一个节点也是正常的情况(非取消) if (next != null && next.waitStatus <= 0) compareAndSetNext(pred, predNext, next); } else { // 尝试失败,只能走比较狠的逻辑去处理了。 unparkSuccessor(node); } node.next = node; // help GC
如果if的判断能通过,那就代表当前这次尝试是成功的,成功了就把链表都链上。
问题来了,为什么这里不把Node.next(取消节点的下一个节点)和Node(当前取消节点)的链给断开,不断开的话,JVM是无法回收掉Node(当前取消节点),那不是内存泄漏了?
Doug Lea这里是不是写的有问题?
nonono.
当正常唤醒节点时,抢到锁的节点会执行
private void setHead(Node node) { head = node; node.thread = null; node.prev = null; }
看到else条件下unparkSuccessor(node);的执行逻辑。
这里已经相当难描述清楚,笔者会执行流程和代码都已经写明白。
这里其实就是一个唤醒操作,唤醒的意义在于去执行shouldParkAfterFailedAcquire()方法从后往前遍历找到一个waitStatus不为1的节点,然后链起来。
cancelAcquire |->unparkSuccessor(node当前取消节点) |->LockSupport.unpark(node.next.thread) |->shouldParkAfterFailedAcquire(node(取消的节点),node.next(取消的节点的下一个节点))
// Node pred 是取消的节点 // Node node 是取消的节点的下一个节点 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { // int ws = pred.waitStatus; if (ws == Node.SIGNAL) return true; // 遍历找到一个非取消的节点 // 并且把当前取消的节点的下一个节点与找到的非取消节点双向链起来。 if (ws > 0) { do { // 往前走 node.prev = pred = pred.prev; // 链起来 } while (pred.waitStatus > 0); // 循环条件,所以找到一个小于等于0的节点就会退出 pred.next = node; // 链起来 } else { compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }
最终唤醒节点,走到shouldParkAfterFailedAcquire()方法中。从后往前的遍历找到正常的节点
到此这篇关于AQS核心流程解析cancelAcquire方法的文章就介绍到这了,更多相关AQS cancelAcquire内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!