概述
AQS是用来构建锁或者其他同步器组件的重要基础框架,是JUC体系的基石。通过内置的FIFO(先进先出队列,CLH队列,是一个单向链表,AQS的队列是CLH的一个变体,虚拟的双向FIFO)队列来完成资源获取线程的排队工作,并通过一个int类型的变量表示持有锁的状态。ReentrantLock、ReentrantReadWriteLock、CountDownLatch、Semaphore等底层都是存在一个内部类继承了AbstractQueuedSynchronizer,为这些具体的并发工具提供了一种排队等候机制。
如果共享资源被占用,就需要一定的阻塞、等待、唤醒机制来保证锁的合理分配,这个机制就是使用CLH队列的变体实现的,将暂时获取不到锁的线程加入队列中等候,这就是AQS的抽象表现之一。将获取不到锁的线程封装成队列中的节点(node),通过CAS、自旋、LockSupport.park()等方式维护state变量的状态,使并发达到同步的控制效果。
AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作,将每条要去抢占资源的线程封装成一个Node节点来实现锁的分配, 通过CAS完成对state值的修改。
state & Node
CLH变种的双向队列
队列Javadoc
The wait queue is a variant of a "CLH" (Craig, Landin, and Hagersten) lock queue.
CLH locks are normally used for spinlocks.
We instead use them for blocking synchronizers, but use information about a thread in the predecessor of its node.
A "status" field in each node keeps track of whether a thread should block.
A node is signalled when its predecessor releases.
Each node of the queue otherwise serves as a specific-notification-style monitor holding a single waiting thread.
The status field does NOT control whether threads are granted locks etc though.
A thread may try to acquire if it is first in the queue.
But being first does not guarantee success;it only gives the right to contend. So the currently released contender thread may need to rewait.
机翻(有道词典):
等待队列是“CLH”(Craig、Landin和Hagersten)锁队列的变体。
CLH锁通常用于自旋锁。
相反,我们将它们用于阻塞同步器,但使用关于其节点的前身中的线程的信息。
每个节点中的“status”字段用于跟踪线程是否应该阻塞。
当它的前身释放时,一个节点被通知。
否则,队列中的每个节点都充当一个特定通知样式的监视器,该监视器持有单个等待线程。
status字段不控制线程是否被授予锁等等。
如果线程是队列中的第一个,那么它可能会尝试获取。
但是第一并不能保证成功,它只是给了你竞争的权利。因此,当前发布的竞争者线程可能需要重新等待。
整个AQS结构图
waitStatus
- 初始化:0
- CANCELLED:1,表示线程获取锁的请求已经取消了
- CONDITION:-2,表示节点在等待队列中,节点线程等待被唤醒
- PROPAGETE:-3,当前线程处于SHARED情况下,该字段才会使用
- SIGNAL:-1,表示当前节点的线程已经准备好了,就差资源的释放了
原理图
通过ReentrantLock解读AQS源码
Lock接口的实现类,基本都是通过聚合了一个队列同步器的子类完成了线程访问的控制。ReentrantLock的部分源码如下:
/** Synchronizer providing all implementation mechanics */
private final Sync sync;
/**
* Base of synchronization control for this lock. Subclassed
* into fair and nonfair versions below. Uses AQS state to
* represent the number of holds on the lock.
*/
abstract static class Sync extends AbstractQueuedSynchronizer {......}
/**
* 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();
}
/**
* Sync object for non-fair locks
*/
static final class NonfairSync extends Sync {......}
/**
* Sync object for fair locks
*/
static final class FairSync extends Sync {......}
public void lock() {
sync.lock();
}
public void unlock() {
sync.release(1);
}
ReentrantLock公平锁
ReentrantLock非公平锁
非公平锁在每个线程尝试lock资源的时候,首先回去CAS修改state的状态,这就是ReentrantLock非公平锁实现中,"非公平"的体现。
可以看到两者的却别仅在于公平锁中多了一个hasQueuedPredecessors的判断,用于判断当前是否存在有效等待队列。
从非公平锁入手解析AQS
ReentrantLock加锁分为三个阶段:
- 尝试加锁
- 加锁失败,线程进入队列
- 线程入队之后进入阻塞状态
尝试枷锁
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
compareAndSetState方法使用CAS的方式去更新state的值。实质上就是尝试加锁,加锁的时候当前线程期望state=0(没有被人占用),如果state=0,那么就将其改变为1。如果CAS操作成功,就代表了当前资源已经被占用,其他线程无法进行加锁,但是此时,加锁操作并未完成。只有当setExclusiveOwnerThread方法执行完毕之后,当前线程才真正的加锁成功。
重点来了,此时的state=1,已经被某个线程占用。在此之后的其他线程再次尝试加锁的时候将会进入acquire方法。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire(arg)方法,该方法的代码可能会让第一次看到的人产生疑惑:
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
tryAcquire直接抛出了异常,这是什么情况?这是一种模板方法设计模式,逼着子类去重写该方法。让子类去做实际上的逻辑实现。因为当前ReentrantLock初始化的是非公平锁NonfairSync,该子类的方法如下:
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
其又调用了父类的Sync的nonfairTryAcquire方法:
final boolean nonfairTryAcquire(int acquires) {
//当表的就是“其他线程”正在尝试加锁的线程
final Thread current = Thread.currentThread();
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;
}
此时由于现在已经有线程占用了资源,c = 1,所以将会进行当前线程与目前占用资源的线程对比,看看是不是同一个线程,此时比较失败,该方法将会返回false。所以回到acquire方法,!tryAcquire(arg) = true。
那么在nonfairTryAcquire方法中c == 0的if判断是干什么呢?
其实存在一种情况,当某个线程尝试compareAndSetState加锁失败后,进入了nonfairTryAcquire方法,此时之前占用的线程正好释放了资源,state == 0,那么当前线程运气很好,正好可以再次尝试CAS加锁。
那么如果当前线程 == 当前占用资源的线程做了什么事情呢?
还是存在一种特殊情况,就是占用资源的线程再次进行了lock操作,尝试再一次的加锁,所以当前nextc = c + 1,之后setState(nextc)设置新的state值,这就是ReentrantLock可重入的锁的实现。
继续刚才非资源占用线程的分析,将会进入acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法。
addWaiter方法
private Node addWaiter(Node mode) {
//将没有抢到锁的线程分装为Node节点
Node node = new Node(Thread.currentThread(), mode);
// 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)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
首先该方法二话不说,就将当前没有抢占用锁的线程封装为Node节点。
pred = tail来获取当前链表的尾节点。如果当前没有等待的线程,那么此时pred == tail == null,此时线程将会进入enq(node)方法:
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
没明显enq方法是一个自旋操作,此时判断链表的尾指针tail是否为空,也就是判断此时链表有没有被初始化过。
- 如果链表没有被初始化过,此时创建了一个空节点通过CAS操作设置为head节点。被称为是傀儡节点或者是哨兵节点,起到占位的作用,与整个逻辑体系关系不大。
- 该head节点thread = null,waiteStatus = 0(int类型默认值)
- 之后又将tail节点指向head节点,也就是刚刚创建的空节点,此时head和tail都指向了该空节点,形成了一个首尾相接的链表。
- 当第一次循环(第一次循环为了初始化链表)结束后方法并没有结束,进入下一次循环。此时将会进入else代码块,开始将当前线程节点进行入队操作。
- 将当前线程封装的节点的前指针prev指向链表的尾指针。
- 将链表的尾指针tail通过CAS操作指向当前线程封装的node节点。
- 再将tail的next指向当前线程封装的node,此时当前线程入队成功。
此时链表中存在一个傀儡节点和一个等待线程节点,返回addWaiter方法,当下一个线程尝试加锁失败后,此时的pred = tail不为空,进入if判断:将当前线程的prev指向链表中的tail,通过CAS操作再将自己设置为tail尾节点,设置成功后将上一个节点的next指向当前线程节点,此时就完成了入队操作,并不用再进入enq方法。
此后的入队操作将会直接进入else代码块,形成的链表如原理图所示。
acquireQueued方法
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);
}
}
这个filed是什么意思,从finally中可以看出,如果filed = true会取消该节点排队,所以这是一决定该节点是否继续等待的标识。
进入自旋代码块,第一行node.predecessor()很简单,返回该节点的前一个节点。
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
如果当前节点的上一个节点是哨兵节点(也就是说当前节点是链表中第一个线程节点),那么就再次调用tryAcquire方法,看看能不能获取到锁资源:
- 如果此时锁资源没有释放,也就是说tryAcquire返回false。进入(shouldParkAfterFailedAcquire&&parkAndCheckInterrupt)判断。如果此时所资源释放了,那么也就是说tryAcquire成功,返回true,执行出队操作将当前节点移出链表。
此时shouldParkAfterFailedAcquire(哨兵节点,当前节点):
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
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.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
由于perd是哨兵节点的关系,此时ws = pred.waitStatus = 0。此时进入else代码块,通过CAS操作将哨兵节点的waitStatus设置为SIGNAL(也就是-1)。返回false,代表这个节点可能没必要继续park了。
进入下一次循环,再次执行tryAcquire尝试获取锁资源。如果还是获取不到再次进入(shouldParkAfterFailedAcquire&&parkAndCheckInterrupt)判断,此时ws = -1,也就是SIGNAL,直接返回true,开始执行parkAndCheckInterrupt内代码:
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
当前节点通过LockSupport挂起,也就是说链表的第二个节点,即第一个线程节点被阻塞,等待着被唤醒。这一步的意义很重要!!!之后的节点也会执行到这一步,被挂起,可以理解为节点在这一步彻彻底底的“入队”了,此时程序不会往下执行了。
ReentrantLock.unlock()
所有队列中的节点都被挂起,等待着锁资源的释放。
ReentrantLock.unlock()
public void unlock() {
sync.release(1);
}
AQS
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
//又是一个模板方法模式,需要子类进行重写
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
ReentrantLock.release()
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
每次unlock都会让state - 1,由于ReentrantLock是可重入锁,之前有提到每一次lock都会+1。当state减到0的时候,free将会 = true返回,将当前锁资源占用的线程置空。
此时回到release方法,进入if判断内,头节点不为null,哨兵节点在shouldParkAfterFailedAcquire方法执行的时候就变为SIGNAL即-1,整体判断(h != null && h.waitStatus != 0) == true,进入判断内执行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.
*/
int ws = node.waitStatus;
if (ws < 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.
*/
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;
}
if (s != null)
LockSupport.unpark(s.thread);
}
此时哨兵节点的state == -1,即ws == -1,所以就unparkSuccessor再通过CAS操作把哨兵节点的state设置为0。
s为哨兵节点的下一个节点,即第一个线程节点。s肯定不等于null,s的state也不大于0。直接执行LockSupport.unpark(s.thread)。【也就是唤醒了之前在parkAndCheckInterrupt方法中阻塞的线程】。
回到acquireQueued方法
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);
}
}
所以parkAndCheckInterrupt方法继续执行,线程没有被中断过,返回false,判断不成立,进入下一次自旋,p仍然是哨兵节点,也就是head指向的节点,再次执行tryAcquire方法试图加锁,此时执行tryAcquire就和之前不太一样了:
final boolean nonfairTryAcquire(int acquires) {
//当表的就是“其他线程”正在尝试加锁的线程
final Thread current = Thread.currentThread();
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;
}
由于上一个线程已经unlock,在parkAndCheckInterrupt阻塞的线程开始了一下次的自旋,这里的current就是队列中的等待线程,所以c == 0。
由于之前上一个持有锁的线程unlock之后,unparkSuccessor通过CAS操作把哨兵节点的state设置为0,所以compareAndSetState(0, acquires) == true,并将当前线程设置为当前占用线程,方法将会返回true。
acquireQueued方法中p == head && tryAcquire(arg) == true,所以进入if代码块中:
- 将当前线程节点设置为head,将thread和前置节点置空,相当于这个节点从阻塞到unlock竞争到锁之后,把这个节点变成了【新的哨兵节点】。
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
- 之前的哨兵节点通过 p.next == null,将没有任何引用,会被GC处理掉。
- failed = false,最后finally将不会取消排队。
- 没有被打断过,interrupted = false,方法返回false。