一文疏通 AQS 到 ReentrantLock
AQS 是什么
AQS 全称是AbstractQueuedSynchronizer, 是juc 下一个核心的抽象类,用于构建各种同步器和锁
比如我们熟悉的 ReentrantLock、ReadWriteLock、CountDownLatch等等是基于AQS
工作原理
下面这张图很清晰了反映了 AQS 的工作原理

当线程获取锁时,即试图对state变量做修改,如修改成功则获取锁;
如修改失败则包装为节点挂载到队列中,等待持有锁的线程释放锁并唤醒队列中的节点
首先在AQS 里面,有几个核心的组成
● state: 共享资源的状态
● 以Node节点组成的双端队列------CLH
● 两个维护队列的Node节点head 和 tail
AQS 基本的属性(源码)
java
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
//头节点
private transient volatile Node head;
//尾节点
private transient volatile Node tail;
//同步状态
private volatile int state;
static final class Node {
//节点状态
volatile int waitStatus;
//前驱节点
volatile Node prev;
//后继节点
volatile Node next;
//当前节点所代表的线程
volatile Thread thread;
//等待队列使用时的后继节点指针
Node nextWaiter;
}
}
这里的Node 的 waitStatus 需要留意一下:
该字段共有5种取值:
● CANCELLED = 1。节点引用线程由于等待超时或被打断时的状态。
● SIGNAL = -1。后继节点线程需要被唤醒时的当前节点状态。当队列中加入后继节点被挂起(block)时,其前驱节点会被设置为SIGNAL状态,表示该节点需要被唤醒。
● CONDITION = -2。当节点线程进入condition队列时的状态。(见ConditionObject)
● PROPAGATE = -3。仅在释放共享锁releaseShared时对头节点使用。(见共享锁分析)
● 0。节点初始化时的状态
独占锁分析
acquire(int)
java
public final void acquire(int arg) {
// tryAcquire需实现类处理
// 如获取资源成功,直接返回
if (!tryAcquire(arg) &&
// 如获取资源失败,将线程包装为Node添加到队列中阻塞等待
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 如阻塞线程被打断
selfInterrupt();
}
acquire核心为tryAcquire、addWaiter和acquireQueued三个函数,其中tryAcquire需具体子类实现。 每当线程调用acquire时都首先会调用tryAcquire,失败后才会挂载到队列,因此acquire实现默认为非公平锁(后到的线程可能会通过这里的tryAcquire 直接拿到锁)
addWaiter将线程包装为节点 node,尾插式加入到队列中,如队列为空,则会添加一个空的头节点。值得注意的是addWaiter中的enq方法,通过CAS+自旋的方式处理尾节点添加冲突。
java
// sync队列中的结点在独占且忽略中断的模式下获取(资源)
final boolean acquireQueued(final Node node, int arg) {
// 标志
boolean failed = true;
try {
// 中断标志
boolean interrupted = false;
for (;;) { // 无限循环
// 获取node节点的前驱结点
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())//
//shouldParkAfterFailedAcquire只有当该节点的前驱结点的状态为SIGNAL时,才可以对该结点所封装的线程进行park操作。否则,将不能进行park操作。
//parkAndCheckInterrupt首先执行park操作,即禁用当前线程,然后返回该线程是否已经被中断
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);// 直接把这个节点置空了
}
}
acquireQueue在线程节点加入队列后判断是否可再次尝试获取资源,如不能获取则将其前驱节点标志为SIGNAL状态(表示其需要被unpark唤醒)后,则通过park进入阻塞状态
release(int)
java
public final boolean release(int arg) {
if (tryRelease(arg)) { // 释放成功
// 保存头节点
Node h = head;
if (h != null && h.waitStatus != 0) // 头节点不为空并且头节点状态不为0
unparkSuccessor(h); //释放头节点的后继结点
return true;
}
return false;
}
private void unparkSuccessor(Node node) {
//这里,node一般为当前线程所在的结点。
int ws = node.waitStatus;
if (ws < 0)//置零当前线程所在的结点状态,允许失败。
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;//找到下一个需要唤醒的结点s
if (s == null || s.waitStatus > 0) {//如果为空或已取消
s = null;
for (Node t = tail; t != null && t != node; t = t.prev) // 从后向前找。
if (t.waitStatus <= 0)//从这里可以看出,<=0的结点,都是还有效的结点。
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);//唤醒
}
release流程较为简单,尝试释放成功后,即从头结点开始唤醒其后继节点,如后继节点被取消,则转为从尾部开始找阻塞的节点将其唤醒。阻塞节点被唤醒后,即进入acquireQueued中的for(;;)循环开始新一轮的资源竞争
总结
上述内容仔细看完,已经可以理解基本的AQS 工作原理了,但是我们的实现类需要实现两个try 的方法(因为默认是直接抛异常,这可不行),所以下面的ReentrantLock 其实并没有添加很多的锁的细节,基本上用的还是AQS 这套锁的流程,无疑就是进行了公平与非公平的分类,以及实现对应的try 方法
ReentrantLock的类结构
ReentrantLock是Lock接口的实现类,基本都是通过聚合了一个队列同步器(AQS)的子类完成线程访问控制的
ReentrantLock 实现了 Lock 接口,有三个内部类,其中 Sync 继承自 AQS ,而后两者继承自 Sync ,它们都继承了 AQS 的能力。
公平锁与非公平锁
构造函数
java
//生成一个公平锁
static Lock lock = new ReentrantLock(true);
//生成一个非公平锁
static Lock lock = new ReentrantLock(false);
static Lock lock = new ReentrantLock();//默认参数就是false
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();//FairSync表示公平锁,NonfairSync表示非公平锁
}
具体实现tryAcquire

其公平的核心方法:
java
/* 判断等待队列中是否存在等待中的线程 */
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
// 判断是否存在先等待的线程 具体分析如下
return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
}
公平锁和非公平锁的lock()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:hasQueuedPredecessors()-----公平锁加锁时判断等待队列中是否存在有效节点的方法
tryRelease
java
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
// 获取独占锁的线程才可以释放线程
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// state == 0 表示所有锁已释放
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
总结
ReentrantLock 实现了 Lock 接口,有三个内部类,其中 Sync 继承自 AQS ,而后两者继承自 Sync ,它们都继承了 AQS 的能力。
本质上来说 ReentrantLock 的底层原理就是 AQS 。
在 Sync 的两个子类 FairSync 和 NonfairSync 分别是公平锁策略和非公平锁策略的实现(公平锁多了一个检查是否有其他等待线程的条件)。然后实现了不同的 tryAcquire(int acquires) ,从而在线程尝试获取锁时,执行不同的策略
参考:
Java 多线程并发 【10】ReentrantLock - 掘金 (juejin.cn)