AQS 独占锁中断场景感慨

698 阅读1分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情

独占锁模式如何被中断唤醒

我们在学习线程时就已经知道线程在处于WAITTING/TIMEDWAITEING 状态时,可以通过interrupt()方法进行唤醒处于等待中的线程; 那么在AQS的独占锁模式下我们如何让处于等待的线程响应中断呢:

private void doAcquireInterruptibly(int arg)
   throws InterruptedException {
   final Node node = addWaiter(Node.EXCLUSIVE);
   boolean failed = true;
   try {
       for (;;) {
           final Node p = node.predecessor();
           if (p == head && tryAcquire(arg)) {
               setHead(node);
               p.next = null; // help GC
               failed = false;
               return;
           }
           if (shouldParkAfterFailedAcquire(p, node) &&
               parkAndCheckInterrupt())
               throw new InterruptedException();
       }
   } finally {
       if (failed)
           cancelAcquire(node);
   }
}

// 唤醒处于等待的线程
private final boolean parkAndCheckInterrupt() {
   LockSupport.park(this);
   return Thread.interrupted();
}

从上述方法来看,当处于等待的线程被唤醒时,会调用 interrupted()方法判断线程是否被中断过,若被中断过则返回true,并清除中断标记; 然后抛出中断异常;

接着中断的节点会进入取消流程

private void cancelAcquire(Node node) {
    if (node == null)
        return;
    node.thread = null;

    Node pred = node.prev;
    while (pred.waitStatus > 0)
        node.prev = pred = pred.prev;
    Node predNext = pred.next;
    node.waitStatus = Node.CANCELLED;
    if (node == tail && compareAndSetTail(node, pred)) {
        compareAndSetNext(pred, predNext, null);
    } else {
     
        int ws;
        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
    }
}

该方法做了2件事:

  1. 将当前取消的节点状态设置为CANCEL,线程设置为null;
  2. 将当前取消节点的前一个非取消节点的next指针指向当前节点的next节点;

模拟4个线程进行抢占锁的场景

假设线程t1先获取锁,其他线程均处于等待状态

image.png

线程t3中断,进行取消

image.png

  1. 第一张图为初始图
  2. 第二章图为取消后的状态
线程t1释放锁,唤醒t2,t2抢占锁

image.png

线程t2释放锁,准备唤醒后续非取消的节点
private void unparkSuccessor(Node node) {
 
    int ws = node.waitStatus;
    if (ws < 0)
    // 首先设置当前节点的状态为0
        compareAndSetWaitStatus(node, ws, 0);

 
 // 从后向前找到最后一个节点状态<=0的节点,并唤醒
    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)
        LockSupport.unpark(s.thread);
}
为什么要从后向前唤醒

首先我们再构建双向链表时,先设置prev,再设置next,(假设某个抢占锁的线程处于t1-t2时刻)那么这个链表就无法构造,从而导致断链;

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) {
    // t1
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
        // t2
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

其次,在某个节点因为中断被取消时;链路结构完整性只能从后往前才能保持完成;

线程t2释放锁,怎么唤醒

从后向前找到最后一个节点状态为<=0的节点 t4;

private void unparkSuccessor(Node node) {
    /*
     * If status is negative (i.e., possibly needing signal) try
     * to clear in anticipation of signalling.  It is OK if this
     * fails or if status is changed by waiting thread.
     */
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    /*
     * Thread to unpark is held in successor, which is normally
     * just the next node.  But if cancelled or apparently null,
     * traverse backwards from tail to find the actual
     * non-cancelled successor.
     */
    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)
        LockSupport.unpark(s.thread);
}

线程t4被唤醒后

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.
         */
        return true;
    if (ws > 0) {
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*
         * 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;
}

image.png

此时t4进而抢占锁成功后,节点进一步变化

image.png

中断节点如何gc

由于上一步节点t4抢占锁成功后,原来的head和t3节点互相指向,但并不被GcRoot所指向, 进而可以被回收;