我们都知道在AQS内部维护着一个FIFO等待队列,而且只有当产生资源竞争的时候才会形成队列,那么它的过程究竟是怎样的呢?让我们来一探究竟吧!
我们来模拟一个情景:
假设现在 t1 线程已经通过 cas 持有锁,即 state = 1,exclusiveOwnerThread = t1,且此时 t2 想要获取锁,那么就会形成队列
入队过程
private Node addWaiter(Node mode) {
//将 t2 线程包装为node
Node node = new Node(Thread.currentThread(), mode);
//尝试进行一次入队操作,如果失败了就进入enq
//在第一次入队的时候肯定是失败的,因为还没有初始化队列
Node pred = tail;
//如果存在尾节点,进入条件
if (pred != null) {
node.prev = pred;
//如果存在队尾元素,则设置当前节点为尾节点
if (compareAndSetTail(pred, node)) {
//pred <- node
pred.next = node;
return node;
}
}
//t2 enter queue
enq(node);
return node;
}
private Node enq(final Node node) {
//会执行两次循环,第一次创建头节点,第二次将 t2 追加到头结点后面
for (;;) {
Node t = tail;
//初始化队列,new 一个 Thread=null 的节点作为头结点
if (t == null) {
if (compareAndSetHead(new Node()))
tail = head;
} else {
//head <- t2
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
第一次循环后,会创建一个 Thread = null 的头结点,且 head 和 tail 都指向头结点
第二次循环后,将 t2 追加到头结点后面,并且将尾指针指向 t2,头节点和 t2 变成双向链表
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
//获得 t2 的前驱节点
final Node p = node.predecessor();
//如果是头节点,则尝试获取锁,因为此时可能 t1 已经释放了锁
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//如果 t2 获取锁失败,那需要进入阻塞
//注意!!该方法第一次进入时,t2 会把头节点的 waitStatus cas(0,-1)
//并返回 false,在下次循环中才会真正阻塞 t2
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
第一次循环修改头节点的 waitStatus,由 0(default) -> -1(signal),表示节点出队后需要唤醒后续节点
第二次循环才调用 LockSuppurt.park(),真正将 t2 阻塞
此时,t2 的入队就完成了,总结一下就是:
- 将 t2 线程包装为 node 节点
- new 出一个 Thread = null 的头节点,将 t2 追加到头结点后面,建立引用关系
- 将头结点的 waitStatus 改为 signal,阻塞 t2
出队过程
public final boolean release(int arg) {
//set state=0,setExclusiveOwnerThread(null)
if (tryRelease(arg)) {
Node h = head;
//存在头节点,且此时头节点的 waitStatus 等于 -1
if (h != null && h.waitStatus != 0)
//唤醒后继节点
unparkSuccessor(h);
return true;
}
return false;
}
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
//将头结点的waitStatus设为0,避免其他线程又来唤醒
compareAndSetWaitStatus(node, ws, 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;
}
//唤醒 t2
if (s != null)
LockSupport.unpark(s.thread);
}
此时,t2 被唤醒后,又继续执行 acquireQueued 方法,进入循环
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
//获得 t2 的前驱节点
final Node p = node.predecessor();
//尝试获取锁成功,因为 t1 已经释放了锁,持有锁的线程变成 t2
if (p == head && tryAcquire(arg)) {
//将 t2 设置为头节点,修改引用关系
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
...
} finally {
if (failed)
cancelAcquire(node);
}
}
此时,t1 出队就完成了,总结一下就是:
- 修改 t1 的 waitStatus 为 0,避免多个线程同时进行唤醒操作
- 唤醒 t2 后,尝试获取锁成功,即
state = 1,exclusiveOwnerThread = t2 - 将 t2 设为头节点,head 指向 t2,并将 Thread = null