AQS同步队列简单分析
AQS
AQS即AbstractQueuedSynchronizer抽象的队列同步器。使用模板模式,通过Unsafe类提供的原子性操作,park、unpark和volatile关键字使变量可见的特性,再加上一个先进先出的队列,实现了对线程的管理。只需要继承该类并重写对应方法,就可以轻松实现对线程的同步操作。
AQS内部有三个private volatile修饰的属性,分别是 state、head、tail。state用来表示各个线程要竞争的共享资源,tail和head分别指向AQS维护的先入先出队列的尾节点和头节点。tail通过CAS和自旋方式保证多线程情况下安全入队,head节点指向的节点是一个哨兵节点,该节点中并没有线程,它的主要职责是唤醒后继节点。tail和head都是延迟初始化。子类继承AQS,需要自己重写相应方法,对state进行判断自己实现获取锁的逻辑和释放锁的逻辑,AQS会将没有获取锁的线程park,在一个线程释放锁后,自动唤醒等该队列中head节点的后继节点(如果后继节点为空或者被取消,则从尾节点开始向前遍历,找到第一个不为空且不等于head节点的节点,如果该节点的状态是要被唤醒状态,则将其唤醒)
AQS维护的CLH队列是一个双向链表,每个节点包含了一个waitStatus属性,用来描述节点的状态,正常的同步节点,初始化时waitStatus=0,waitStatus>0表示节点由于中断或者超时而被取消
下面是acquire的加锁流程
// 独占模式加锁 大致流程为:如果获取锁失败,则加入等待队列
public final void acquire(int arg) {
if (!tryAcquire(arg) && // tryAcquire 子类必须实现,自己控制是否能够获取锁。true代表该线程获取锁成功、false则获取锁失败
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 将当前线程包装为一个Node节点,并从队尾入队
selfInterrupt();
}
// 节点入队
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode); // 将当前线程包装为一个Node,Node的waitStats=0
// 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)) { // CAS操作将新加入的Node置为尾节点
pred.next = node;
return node;
}
}
enq(node); // 如果快速入队失败或者队列没有初始化,执行常规入队
return node;
}
//使用自旋方式,不断尝试入队,直到入队成功
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize 初始化操作
if (compareAndSetHead(new Node()))
tail = head; // 初始化时Node的waitStats=0 head节点是一个哨兵节点,并不存储线程
} else { // 入队操作
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
// 将Node包裹的线程挂起
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true; // 是否失败标记
try {
boolean interrupted = false; // 是否发生中断
for (;;) {
final Node p = node.predecessor(); // 获取当前节点的前置节点
/**
在第一个if中限制了在什么时候一个线程可以尝试获取锁的操作(调用tryAcquire(arg)获取锁)
即线程必须是队列中第二个节点(因为队列中第一个节点为哨兵节点)
该操作可以防止node中线程被虚假唤醒,即使被虚假唤醒,也会被重新的挂起
*/
if (p == head && tryAcquire(arg)) { // 只有前置节点为head节点,该节点才有资格执行tryAcquire方法抢夺锁
setHead(node); // 一旦当前节点获得了锁,则出队,置为head节点
p.next = null; // help GC 断开原来head,在下一次垃圾回收时被收集
failed = false; // 操作成功
return interrupted; // 传递线程的中断信息
}
if (shouldParkAfterFailedAcquire(p, node) && // 只有线程的前一个节点的waitStatus是-1时,后面一个节点才会被挂起,新加入的节点如果没有其它操作
//节点状态都默认是0,通过一次shouldParkAfterFailedAcquire调用会将前一个节点状态置为-1,在下一次循环中该方法将会返回true
parkAndCheckInterrupt()) // 挂起线程并且检查线程是否被打断
interrupted = true; // 只有线程发生了中断才可能走到这一步,最后会把中断信息传递出去
}
} finally {
if (failed) //如果没有按照约定的规则获取到锁,failed 就会为true
cancelAcquire(node);
}
}
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); // 执行挂起操作,保存挂起时线程的现场信息。线程在此处被挂起,等待其它线程的唤醒。
return Thread.interrupted(); // 判断线程是否被中断
}
// 释放锁
public final boolean release(int arg) {
if (tryRelease(arg)) { // 若要使用release方法,子类必须重写tryRelease,由子类控制锁的释放,AQS负责将等待队列中头节点的下一个线程唤醒
Node h = head;
//如果该条件不成立,说明等待队列中没有需要唤醒的节点。通过前面分析知道如果一个节点要被park,那么它前一个节点必须是-1
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); // 唤醒头节点的下一个节点
return true;
}
return false;
}
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)
// 尝试将head节点waitStatus置为0,如果node.next中线程获取到锁,那么这个头节点就会被替换为node.next,如果没有获取倒锁,node.next将会被再次park,
// 且waitStatus还会被置为-1(这个逻辑在acquireQueued中)
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) { // 如果head节点的下一个节点不可用,那么从尾节点开始向前遍历,知道找到第一个可用的node
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); // 唤醒节点。但是该节点并不一定能获取到锁,如果是非公平锁,那么该节点中的线程还要和新加线程竞争
}

浙公网安备 33010602011771号