持续创作,加速成长!这是我参与「掘金日新计划 · 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件事:
- 将当前取消的节点状态设置为CANCEL,线程设置为null;
- 将当前取消节点的前一个非取消节点的next指针指向当前节点的next节点;
模拟4个线程进行抢占锁的场景
假设线程t1先获取锁,其他线程均处于等待状态
线程t3中断,进行取消
- 第一张图为初始图
- 第二章图为取消后的状态
线程t1释放锁,唤醒t2,t2抢占锁
线程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;
}
此时t4进而抢占锁成功后,节点进一步变化
中断节点如何gc
由于上一步节点t4抢占锁成功后,原来的head和t3节点互相指向,但并不被GcRoot所指向, 进而可以被回收;