AQS队列构建源码解读
概述
今天来聊聊AQS,AQS全称为AbstractQueuedSynchronizer是一个抽象类,被称为JUC的基石,官方的文档解释为:
为实现依赖于先进先出 (FIFO) 等待队列的阻塞锁和相关同步器(信号量、事件,等等)提供一个框架。此类的设计目标是成为依靠单个原子 int 值来表示状态的大多数同步器的一个有用基础。子类必须定义更改此状态的受保护方法,并定义哪种状态对于此对象意味着被获取或被释放。假定这些条件之后,此类中的其他方法就可以实现所有排队和阻塞机制。子类可以维护其他状态字段,但只是为了获得同步而只追踪使用 getState()、setState(int)
和
compareAndSetState(int, int) 方法来操作以原子方式更新的 int 值。此类支持默认的_独占_ 模式和_共享_ 模式之一,或者二者都支持。处于独占模式下时,其他线程试图获取该锁将无法取得成功。在共享模式下,多个线程获取某个锁可能(但不是一定)会获得成功。此类并不“了解”这些不同,除了机械地意识到当在共享模式下成功获取某一锁时,下一个等待线程(如果存在)也必须确定自己是否可以成功获取该锁。处于不同模式下的等待线程可以共享相同的 FIFO 队列。通常,实现子类只支持其中一种模式,但两种模式都可以在(例如)ReadWriteLock中发挥作用。只支持独占模式或者只支持共享模式的子类不必定义支持未使用模式的方法
AQS定义了一套多线程访问共享资源的队列同步器框架,而其底层更改锁状态均是通过compareAndSetState(int, int)方法,该方法的具体实现规则便是compareAndSwapInt(Object o, long offset, int expected, int x); 也就是常见的CAS操作。
首先我们先理解一下什么叫队列同步器,就方法名而言,显而易见,其自身肯定要有队列数据结构和同步操作两块。接下来我们从数据结构层面聊一聊队列和状态同步操作。
数据结构
AQS数据结构从代码描述来看大致是如下图所示的:
队列拥有head头和tail尾,head头指向第一个节点,tail指向最后一个节点。
节点由等待的线程,waitStatus等待状态,prev前指针,next后指针组成。
有队列的存在自然是因为当前资源被占有,此时蓝色节点线程A正占有着一把锁,state状态为1。当state状态为0锁便会被释放。
state关键字被volatile修饰,其目的是为了保证线程的可见性,保证该状态在多个线程之间是可见的,volatile的具体语义此处就不细说了。
在队列的节点中还有一个特殊的节点,就是橙色区块的null节点,该节点未存储任何线程,它只是一个占位节点,我们把它称为哨兵节点。后面逐步解释整个队列是如何构建的。
特性:
- 独占 独占模式即为仅允许一个线程执行,如常见的ReentrantLock,加锁后仅允许当前线程操作资源。
- 共享 和独占相反允许多个线程操作同一个资源,如Semaphore/CountDownLatch/CyclicBarrier
- 可重入 AQS的状态会因为同一个线程多次获取锁而每次对state值+1这便是可重入的意思,而每次该线程解锁时候也会对state值-1。一个线程对该资源加锁几次,就需要释放几次否则该资源会一直处于加锁状态。
- 公平与非公平 公平锁与非公平锁的区别就在于当线程抢锁的时候,是直接抢还是检查一下队列,队列有线程等待就排队这就是公平锁,直接抢锁的则为非公平锁。此处需要注意AQS的公平锁并非绝对公平。这里引用官方文档的话来说:“对于默认闯入(也称为 greedy、renouncement 和 convoy-avoidance)策略,吞吐量和可伸缩性通常是最高的。尽管无法保证这是公平的或是无偏向的,但允许更早加入队列的线程先于更迟加入队列的线程再次争用资源,并且相对于传入的线程,每个参与再争用的线程都有平等的成功机会。此外,尽管从一般意义上说,获取并非“自旋”,它们可以在阻塞之前对用其他计算所使用的 tryAcquire 执行多次调用。在只保持独占同步时,这为自旋提供了最大的好处,但不是这种情况时,也不会带来最大的负担。如果需要这样做,那么可以使用“快速路径”检查来先行调用 acquire 方法,以这种方式扩充这一点,如果可能不需要争用同步器,则只能通过预先检查 hasContended() 和/或 hasQueuedThreads() 来确认这一点。”
队列构建源码解读
AQS的CLH队列并不是一开始便已经构建好了,这里我们将会通过源码分析AQS同步队列是如何构建出来,如果对队列添加节点的。这里我们会从AQS的子类ReentrantLock来一步步分析,也就是说此处分析的队列模型特性为:可重入,独占。
我们先来看一下模拟场景的代码:
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Thread thread_a = new Thread(() -> {
try {
lock.lock();
System.out.println("线程A干活开始.........");
Thread.sleep(1000);
System.out.println("线程A干活结束.........");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "Thread A");
Thread thread_b = new Thread(() -> {
try {
lock.lock();
System.out.println("线程B干活开始.........");
Thread.sleep(2000);
System.out.println("线程B干活结束.........");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "Thread B");
Thread thread_c = new Thread(() -> {
try {
lock.lock();
System.out.println("线程C干活开始.........");
Thread.sleep(2000);
System.out.println("线程C干活结束.........");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "Thread C");
thread_a.start();
thread_b.start();
thread_c.start();
}
我这里是直接使用main方法运行,一般场景下lock锁一般定义在类里面。
大致解释一下,有ABC三个线程,三个线程需要在系统执行相关业务代码,此处的sleep方法代表执行的业务逻辑,每个线程进来时候获取锁,获取不到则阻塞,业务执行完毕释放锁。一个简单的逻辑。我们此处从构造方法开始看。
1.构造方法构建
/**
* Creates an instance of {@code ReentrantLock}.
* This is equivalent to using {@code ReentrantLock(false)}.
*/
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
我们在调用ReentrantLock构造函数时没有传入参数所有默认走第一个无参构造,而无参构造则直接new了一个非公平Sync同步对象。
2.加锁方法
/**
* 获得锁
*
* <p>如果锁没有被其他线程持有,立即设置锁保持技术为1
*
* <p>如果当前线程已经持有锁,则保持count加1,该方法立即返回。
*
* <p>如果锁被另一个线程持有,则当前线程被禁用,用于线程调度休眠
* 直到获得锁,当驳斥计数设置为1时
*/
public void lock() {
sync.lock();
}
之前创建的sync对象调用了lock方法。sync对象继承于AQS抽象类,加锁方法在其实现中为抽象方法,我们继续看ReentrantLock的具体实现。
此处因为我们直接创建的非公平锁,所以我们查看其NonfairSync下的具体实现。
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
此处compareAndSetState方法将期望值0修改为1,如果修改成功,则调用setExclusiveOwnerThread(Thread.currentThread());方法,否则调用acquire方法。
这时我们是A线程获得锁调用compareAndSetState方法,此时state的状态为0与期望值相符合,然后将其改为1,所以if判断为true,接着调用setExclusiveOwnerThread(Thread.currentThread());方法将该线程设置为持有锁。这个时候我们队列的整个状态为
线程A持有锁并将state值设置为1。此时A开始执行任务,接着B开始获取锁。
因为这个时候state的值已经为1与期望值0不相符,所以CAS交换失败,会走else代码调用acquire(1);方法。我们接着看
/**
* Acquires in exclusive mode, ignoring interrupts. Implemented
* by invoking at least once {@link #tryAcquire},
* returning on success. Otherwise the thread is queued, possibly
* repeatedly blocking and unblocking, invoking {@link
* #tryAcquire} until success. This method can be used
* to implement method {@link Lock#lock}.
*
* @param arg the acquire argument. This value is conveyed to
* {@link #tryAcquire} but is otherwise uninterpreted and
* can represent anything you like.
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
我们已经从ReentrantLock跳转到了AbstractQueuedSynchronizer,也就说该方法已经在AQS顶层进行了实现。该方法只会在独占模式下获取锁时使用。tryAcquire调用成功则会成功获取锁,失败则会开始将线程入队。我们接着看tryAcquire方法实现,此时arg为1。state值为1。
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
该方法啥也没干,直接抛出异常,我们需要继续看ReentrantLock的具体实现。
此处有四个实现,我们看ReentrantLock的非公平锁实现。
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
该方法调用nonfairTryAcquire,我们接着往深处追。
final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程也就是B线程的对象引用
final Thread current = Thread.currentThread();
// 获取state状态,此时state为1
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
来到了关键代码块,上面我们得知if判断失败将会走else判断,并调用getExclusiveOwnerThread方法判断当先线程B是否与已经持有锁的线程A相等,此处自然也判断不通过,所以直接返回false,这时回到了上面的acquire方法在if判断取反为true,接着走acquireQueued方法。在此之前我们需要先看该方法内的参数方法**addWaiter **方法,该方法中的参数为Node.EXCLUSIVE 此时是位null的。
private Node addWaiter(Node mode) {
// 调用node对象构造方法,第一个参数为当先线程,也就是线程B,第二对象也为线程B
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
// 将队列尾指针赋值给节点前指针,此时tail尾指针未经过任何处理为null
Node pred = tail;
// 判断前指针是否为null,此处if判断不通过
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
该方法注释释义为,为当前线程和给定模式创建队列和排队。我们终于来到了创建队列的方法。
该方法当前状态下的if判断不通过进入enq方法,并将构建好的包含线程B的node节点传入。
此时的队列状态为下图所示:
我们有了空的头指针和尾指针,以及包含线程B的节点。
我们继续看enq方法
private Node enq(final Node node) {
// 死循环自旋
for (;;) {
// 将尾指针赋值给t,此时tail为null
Node t = tail;
// t为null因此,此处判断通过
if (t == null) { // Must initialize
// 将heda指向new出来的一个ndoe节点
if (compareAndSetHead(new Node()))
// 将尾指针也指向该节点
tail = head;
} else {
// 因为是死循环,当上述if判断执行后此时t不为null了
// 此处再将尾指针指向传入节点的前指针
node.prev = t;
// 接着将尾指针指向包含线程B的节点
if (compareAndSetTail(t, node)) {
// 最后将t也就是前面new出来的节点的后指针指向包含线程B的节点然后返回
t.next = node;
return t;
}
}
}
}
这里注释写的很清楚了,我们直接看队列变化。先是创建了一个node(null)节点,这里不代表节点为null,而是不包含任何线程的节点,然后将头指针和尾指针都指向了这个橙色节点。
然后我们接着看else的后续操作。
else操作中先将绿色节点prev指向了橙色节点,紧接着将tail指针指向绿色节点,最后将橙色节点的next指向绿色节点,图中已经标识了具体顺序。
此时我们的队列已经创建完成。当返回t以后,代码将层层返回,然后跳出addWaiter方法携带绿色节点B调用acquireQueued方法,而第二个参数为1。继续看acquireQueued方法。
final boolean acquireQueued(final Node node, int arg) {
// node为包含线程B节点,arge为1
boolean failed = true;
try {
boolean interrupted = false;
// 自旋
for (;;) {
// 该方法获取绿色节点的前指针,此时前指针为头节点
final Node p = node.predecessor();
// 此处第一个条件判断通过,调用tryAcquire方法传入1,此时A还在干活所以tryAcquire方法会返回false
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 判断不通过调用,shouldParkAfterFailedAcquire方法会将哨兵节点的等待状态设置为-1并返回false,所以该判断也不会执行,将进入下一次循环
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
此处再次调用tryAcquire方法,也就是调用非公平锁的实现,这里为了避免大家绕晕,我在把代码放一遍。
final boolean nonfairTryAcquire(int acquires) {
// 依旧是获取线程B
final Thread current = Thread.currentThread();
// 状态为1
int c = getState();
// c不为0
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 这个时候A还在干活
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
// 直接跳出
return false;
}
上述方法直接跳出,然后返回false,接着走第二个判断,调用shouldParkAfterFailedAcquire方法
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// pred为头指针,node为绿色节点
// 此时绿色指针的状态为等待状态为0,走第三个else
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.
*/
// 此处设置橙色哨兵节点的wait状态为SIGNAL,也就是-1
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
到现在为止,队列只做了一件事情,将橙色哨兵节点的等待状态设置为-1.然后代码返回false,所以上述acquireQueued方法中的判断将会以false结束进入第二次循环,我们看第二次循环
final boolean acquireQueued(final Node node, int arg) {
// node为绿色节点,arge为1
boolean failed = true;
try {
boolean interrupted = false;
// 自旋
for (;;) {
// 依旧获取B节点的前指针,也就是head指针
final Node p = node.predecessor();
// 此处第一个条件判断通过,调用tryAcquire方法传入1,调用tryAcquire方法时如果有其他线程占有锁就返回false
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 判断不通过调用,shouldParkAfterFailedAcquire方法,因为第一次循环已经将哨兵节点的等待状态设置为了SIGNAL,所以将会直接返回true
if (shouldParkAfterFailedAcquire(p, node) &&
// 接着将当前线程B LockSupport.park(this); 也就是中段线程,直到某个地方调用
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
下面是parkAndCheckInterrupt方法
private final boolean parkAndCheckInterrupt() {
// 中断当前线程,直到有人调用unpark方法
LockSupport.park(this);
// 当有人唤醒该线程时,当前线程如果被中断将返回true,否则返回false
return Thread.interrupted();
}
好了,我们已经理清楚了A加锁执行业务到B申请锁进入park中断的状态,那么我们在带入线程C也进入队列,此时C也没有获取到锁也进入了park状态形成了一个拥有三个节点的队列,C入队的源码逻辑和B同理,只是此时C的逻辑中hed和tail已经有了指向,只需要将B节点后指针指向C,并将tail指针指向C,并将C指针的prev前指针指向B,具体的代码大同小异就不放出来了,我们这时在看一下队列的情况。
此时队列就变成了我们最开始那副图的模样,那么接下来我们看解锁逻辑。
3.解锁方法
这个时候A的活终于干完了,要进入解锁逻辑了开始调用
public void unlock() {
sync.release(1);
}
依旧是调用sync非公平的release实现,并传入1.
public final boolean release(int arg) {
// 当state减去arg为0时会返回true,如果说A线程加锁两次,
// 这个时候state为2-1,将返回false,而解锁方法本身没有接收返回值不用管
// 方法加锁几次state就为几,需要全部解锁才能进入这个if判断,这也就是可重入加锁后需要解锁多次的原因
if (tryRelease(arg)) {
// 这里我们已经成功将状态改为0了,进入判断
// 将头节点赋值给h,这个时候头节点肯定是不为null
Node h = head;
// 头节点此时等待状态为-1,不为0判断通过
if (h != null && h.waitStatus != 0)
// 此处唤醒线程,最后返回true,我们这里主要看unparkSuccessor方法
unparkSuccessor(h);
return true;
}
return false;
}
上面的tryRelease的逻辑非常简单,我也写了注释就不细说了
我们接着看unparkSuccessor方法,这个方法才是解锁并操作队列的关键。
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.
*/
// 此处获取头指针等待状态,次时为-1
int ws = node.waitStatus;
if (ws < 0)
// 将头指针指向的节点等待状态改为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.
*/
// 获取头指针指向节点的下一个节点,也就是包含线程B的节点
Node s = node.next;
// 看包含线程B的节点是否为null
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;
}
// 因为我们next指向的节点不为null,所以唤醒线程B
if (s != null)
LockSupport.unpark(s.thread);
}
此时A任务结束,并将线程B唤醒。我们先看一下队列的变化状态。
这里橙色哨兵节点等待状态再次改为0,并且state状态变为0,线程B被唤醒,所以我们需要回到线程B的加锁方法代码。当前轮的自旋循环结束,而且B被唤醒parkAndCheckInterrupt()返回false结束判断。然后我们接着看下一轮循环。
final boolean acquireQueued(final Node node, int arg) {
// node为绿色B节点,arge为1
boolean failed = true;
try {
boolean interrupted = false;
// 自旋
for (;;) {
// 依旧获取绿色B节点的前指针,也就是head指针指向的哨兵节点
final Node p = node.predecessor();
// 此处第一个条件判断通过,再次调用tryAcquire方法传入1,这个时候当前线程B可以持有锁,因为线程A已经释放了所以返回true
if (p == head && tryAcquire(arg)) {
// 将绿色B节点赋值给head,并把绿色B节点的前指针置位null,将B节点线程置位null
setHead(node);
// 此时p是哨兵节点,将哨兵节点的后指针next也指向null,这个时候哨兵节点不被任何节点引用
p.next = null; // help GC
failed = false;
// 最后此处返回false
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
我们在看一眼当前队列的变化
这个时候橙色的哨兵节点将因为没有被任何节点引用而被GC回收掉,而第一个绿色节点也会节点内线程被置空成为新的哨兵节点。最后就会变成如下图所示
哨兵节点出队,原来的B节点成为了新的哨兵节点,B也获取到了锁,大家皆大欢喜,后面C获取锁也是一样的流程就不再赘述了。