AQS
原理
AQS核心思想是,如果被请求的共享资源空闲,那么就将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态;如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体(虚拟双向FIFO队列)实现的,AQS是通过将每条请求共享资源的线程封装成一个节点来实现锁的分配,将暂时获取不到锁的线程加入到队列中。也就是说,AQS就是基于CLH队列,用volatile修饰共享变量state,线程通过CAS去改变状态符,成功则获取锁成功,失败则进入等待队列。AQS使用了模板方法,所有的同步类内部都聚合了一个自定义同步器,将加锁解锁的功能委派给自定义同步器来完成,而自定义同步器继承AQS,并重写了指定方法,在这些方法里面完成对共享资源的修改,AQS内部通过调用重写后的方法来完成具体的加锁解锁以及各种队列操作的逻辑。
实现细节
细节:
- 队列的头结点是一个空结点,第二个结点才是真实数据的第一个结点。
- 第一个真实结点是获取同步状态成功的结点。
- 线程在队列中等待获取锁的过程中,并不会去响应中断,只是记录一下中断,最后拿到锁返回时,如果被中断过,则会去补一次中断。
加锁过程:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
会先尝试去获取共享资源(如果是非公平锁则之前还会有一次CAS操作去修改共享资源),如果成功则加锁成功,否则为当前线程创建结点并添加到队列当中尾部,然后进行自旋过程。每一次自旋都会去判断当前结点的前驱节点是否为头结点,如果是则会去尝试获取共享资源,成功则意味着获取锁成功,然后将当前结点设置为头结点(相当于把当前结点从队列中移除),结束自旋,否则继续自旋。同时为了防止无限自旋浪费CPU资源,每次自旋的时候都会去判断是否阻塞当前线程,如果当前结点的前驱结点的状态值为-1,则阻塞当前线程并等待唤醒,如果不是-1,则通过CAS的方式将前驱结点的状态值修改为-1(会先跳过已取消的结点)。
解锁过程:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
会先去尝试释放共享资源,如果成功则去唤醒下一个结点对应的线程,则解锁成功。
推荐阅读:https://tech.meituan.com/2019/12/05/aqs-theory-and-apply.html
如果你想了解更多我对编程和人生的思考,请关注公众号:青云学斋