AQS 学习记录
AQS(AbstractQueuedSynchronizer)抽象同步队列,为线程的同步和等待等操作提供一个基础模板类,JUC并发包中的大部分并发工具类都是基于AQS实现的。
AQS的核心是一个state的状态和一个双向链表。state代表着被抢占的锁的状态,0表示锁未被占用,非0的时候代表有线程获取到了锁,是否可以将state从0设置为1,代表着线程是否可以获取锁成功。如果线程没有获取到锁,则会被包装成一个Node节点,存放到一个双向链表中。
AQS的部分源码如下:
对于链表中的Node,包含如下属性:
static final class Node {
//锁的共享状态
static final Node SHARED = new Node();
//锁的独占状态
static final Node EXCLUSIVE = null;
//当waitStatus为CANCELLED,表示该节点代表的线程已释放(超时、中断),已取消的节点不会在阻塞;
static final int CANCELLED = 1;
//当waitStatus为SIGNAL,该节点的后继节点线程处于等待状态,如果当前节点释放了同步状态或被取消,会通知后继节点,使后继节点可以运行;
static final int SIGNAL = -1;
//当waitStatus为CONDITION,该节点线程在condition队列中阻塞,其他线程对Condition调用了signal()方法后,该节点从等待队列中转移到同步队列中,加入到对同步状态的获取中;
static final int CONDITION = -2;
//当waitStatus为PROPAGATE,表示下一次的共享状态会被无条件的传播下去
static final int PROPAGATE = -3;
//
volatile int waitStatus;
//前驱节点
volatile Node prev;
//后继节点
volatile Node next;
//获取同步状态的线程
volatile Thread thread;
//等待在condition上的下一个节点
Node nextWaiter;
}
当然,还需要知道当前是哪个线程拥有锁。这个定义在AQS的父类(AbstractOwnableSynchronizer)中,用exclusiveOwnerThread来记录当前拥有锁的线程。源码如下:
AQS内部结构
基于上述的内容,可以得到AQS的内部结构如下图:
获取锁
了解了AQS的基本结构,然后看看线程是怎么来获取锁的。下面通过ReentrantLock来解析获取锁的过程。
如上图ReentrantLock的部分源码,ReentrantLock类中的的lock()方法来自Sync的lock()方法,Sync是一个抽象类,只能看其子类的实现,刚好下面就有两个Sync的子类,分别为:FairSync(公平锁)、NonFairSync(非公平锁)。非公平锁的性能高于公平锁,所以ReentrantLock默认是非公平锁,所以下面基于非公平锁分析。
NonFairSync源码如下:
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
当执行lock的时候,先通过CAS的方式尝试获取一次锁 compareAndSetState(0, 1) 。
- 结果为True,表示当前线程获取锁成功,将获取锁的独占线程设置为当前线程;
- 结果为False,表示锁已经被占有,但是还有两种情况:
- 占有锁的就是当前线程,即是锁的重入,后续同样可以获取到锁;
- 占有锁的是其他线程,在其他线程占有锁的期间,当前线程需要放入链表中等待;
结果为false的两种情况,都会执行 acquire(1) 。AQS中acquire方法的源码如下:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
首先看if的条件, tryAcquire(arg) ,在AQS中这个方法的实现是直接抛出异常了。
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
所以得看具体子类的实现来确认获取锁的逻辑,回到ReentrantLock类中的NonfairSync类来看tryAcquire()方法的逻辑。通过上面图中的代码发现,具体的逻辑在 nonfairTryAcquire(int acquires) 方法中。具体的代码如下:
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;
}
具体的流程如下:
1. 首先获取锁的状态,看锁的状态是否为0,也即是否被占用;
2. 如果锁状态等于0表示未被占用,直接获取通过CAS的方式尝试获取锁。成功,就设置拥有锁的线程为当前线程,返回true;失败,直接返回false;
3. 如果锁状态非0表示已被占用,查看拥有锁的线程是否和当前线程是同一个线程;
4. 如果不是同一个线程,直接返回false;
5. 如果是同一个线程,那么表示重入锁,修改锁状态并返回true;
获取锁失败
如果 tryAcquire(arg) 返回True,则说明获取锁成功,acquire(int arg) 直接结束;如果返回false,说明获取锁失败,则需要执行 acquireQueued(addWaiter(Node.EXCLUSIVE), arg) 。
构建链表节点Node并加入链表
首先从里面的 addWaiter(Node.EXCLUSIVE) 方法看起,将当前线程封装成一个Node,加入到链表中。源码如下:
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
//下面为用到的Node中的构造函数的源码
Node(Thread thread, Node mode) {// Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
具体流程如下:
- 先将线程封装成一个链表的Node节点;
- 声明一个指针pred指向当前链表的尾节点;
- 如果尾节点不为空,将新封装的节点以CAS的方式新增到链表尾;
- 新增成功,tail指针执向当前节点,pred.next指向当前节点,返回当前节点,即尾节点;
- 新增失败或者pred为null,走enq(node)的逻辑,然后返回node;
上述流程为尝试快速加入链表,如果成功就直接返回;如果失败(存在竞争),在使用CAS反复加入链表,直到加入成功。加入链表 Node enq(final Node 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;
}
}
}
}
具体流程如下:
- 首先是一个死循环,用来保证要新增的节点成功加入到链表中,如果失败就继续尝试;
- 如果尾节点为null,则使用CAS的方式设置一个节点为头节点,该头节点没有包装任何线程(延时初始化头结点);
- 当尾节点不为空(上一步初始化了头结点或者addWaiter(Node.EXCLUSIVE)方法CAS设置尾节点失败了),继续通过CAS的方式将当前线程的节点设置为尾节点;
- 设置失败就继续尝试;设置成功就返回链表中的倒数第二个元素;
这里可以看出两点:
- 头结点是在加锁时用到的时候采取初始化,并不是在一开始启动的时候就初始化,所以是延时初始化;
- 头节点是不存储任务线程的;
在*Node enq(final Node node)*有如下三行代码:
node.prev = t;//------> 1
if (compareAndSetTail(t, node)) {//------> 2
t.next = node;//------> 3
return t;
}
这部分的代码可有可能是高并发执行的,也就是说,会有不止一个新节点指向尾节点,此时就出现了链表尾分叉的情况。
不过从上述代码的逻辑来看,<1>是一定会执行成功的,只有<2>执行成功了,<3>才会继续执行。那么鉴于CAS的特性,在高并发的时候,只有一个线程会成功执行<1>、<2>、<3>,其他线程支会执行<1>,剩下的<2>、<3>会失败。所以这些线程不会退出,还会执行下次循环,当下次循环的时候这些线程就指向了一个新的尾节点,所以只有执行成功的节点才会被加入链表中,执行失败的节点需要重新尝试CAS操作来完成加入链表的操作,这样也就不会有链表尾分叉的问题了。
入队成功,再尝试获取锁
至此,说明线程获取锁失败并将其封装为Node节点,已经成功加入链表中。然后接下来执行的方法为 acquireQueued(addWaiter(Node.EXCLUSIVE), arg) ,具体的源码如下:
首先看死循环里面的第一个if里面的逻辑。也即是如下的逻辑:
if (p == head && tryAcquire(arg)) {
//如果当前节点的前驱节点是头结点,再尝试获取一次锁。
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
如果加入链表的线程节点的前驱节点是头结点,那么再尝试获取一次锁,因为有可能在当前节点加入链表的过程中,拥有锁的线程使用释放掉了锁。如果这里成功获取了锁,将当前节点设置为头节点,然后将原来的头结点删除掉。返回false。
从代码里可以看出,acquireQueued() 返回的是中断标志,true以为中断过,false表示没有被中断过。现在会看最初的acquire():
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } static void selfInterrupt() { Thread.currentThread().interrupt(); }
如果返回True,表示线程被中断过,调用selfInterrupt()方法,将当前线程中断一下。
如果当前节点的前驱节点不是头结点或者再次尝试获取锁的时候获取失败,则执行以下的逻辑:
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
先从 shouldParkAfterFailedAcquire(p, node) 的源码开始,代码如下:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
具体流程如下:
- 先查看前驱节点的状态,如果前驱节点的状态为SIGNAL,表示此节点可以挂起,所以直接返回true。如果前驱节点状态为SINGAL,会在适当的时候唤醒前驱节点的后继节点,也就是当前节点,具体可参考文章开头对Node节点中waitStatus属性的描述;
- 如果前驱节点的状态大于0(即为CANCELLED),说明前驱节点已经被释放,不阻塞参与锁的竞争,此时继续向前找,找到状态不为CANCELLED的节点,设置为当前节点的前驱节点,返回false;
- 如果前驱节点状态不是以上两种,将前驱节点的状态通过cas的方式设置为SIGNAL,返回false;
回到if条件的代码位置,如果shouldParkAfterFailedAcquire方法返回true,说明允许当前线程挂起来,并执行 parkAndCheckInterrupt() 来挂起当前线程,该方法的源码如下:
private final boolean parkAndCheckInterrupt() {
//挂起当前线程,线程不再继续执行,等待被unpark唤醒,然后再继续执行。
LockSupport.park(this);
return Thread.interrupted();
}
在acquireQueued方法的最后有一个finally代码块,处理一些异常的情况,源码如下:
private void cancelAcquire(Node node) {
// 1.过滤为null的节点,不做处理。
if (node == null)
return;
//2.当前节点的线程为null,从当前节点向前找第一个SIGNAL状态的节点设置为当前节点的前驱节点
node.thread = null;
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
Node predNext = pred.next;
//3.将当前节点的状态设置为CANCELLED
node.waitStatus = Node.CANCELLED;
//4.如果当前节点是尾节点,上一步已经设置为CANCELLED,将第二步中找到的SIGNAL状态的节点设置为尾节点
if (node == tail && compareAndSetTail(node, pred)) {
//5.如果设置成功,将尾节点后面的节点设置为null
compareAndSetNext(pred, predNext, null);
} else {
int ws;
if (pred != head &&//如果当前节点的前驱节点不是头结点
((ws = pred.waitStatus) == Node.SIGNAL ||//当前节点的前驱节点的状态为SIGNAL
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&//或者前驱节点的状态不为CANCELLED且可以设置为SIGNAL
pred.thread != null) {//前驱节点的线程不为null。
Node next = node.next;
if (next != null && next.waitStatus <= 0)
//如果当前节点的后驱节点不为空且状态不为CANCELLED,将当前节点的后继节点设置为当前节点前驱节点的后继节点。
compareAndSetNext(pred, predNext, next);
} else {
//唤醒当前节点下一个节点
unparkSuccessor(node);
}
//当前节点的后继节点指向自己
node.next = node; // help GC
}
}
大致的流程如下:
获取锁总结
至此,获取锁的过程基本就完成了,基本总结下流程,归纳如下:
- 快速获取锁,当前没有线程持有锁的时候,直接获取锁;
- 尝试获取锁,当没有线程执行或当前线程占有锁,可以直接获取锁;
- 获取锁失败后,将当前线程包装成Node节点,加入链表,设置尾几点;
- 如果当前节点的前驱节点为头结点,再尝试获取一次锁;
- 将当前节点的前一个有效线程的状态设置为SIGNAL;
- 然后阻塞,等待唤醒;
释放锁
释放锁 unlock() 的代码如下:
public void unlock() {
sync.release(1);
}
真正的逻辑是从AQS中的 release(int arg) 开始的,具体的代码如下:
首先从 tryRelease(arg) 开始,源码如下:
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
//当前线程不是持有锁的线程,抛出异常。
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
//如果状态减去releases之后为0,说明非当前线程不再占有锁,返回true,将当前占有锁的线程设置为null。
free = true;
setExclusiveOwnerThread(null);
}
//如果状态更新之后不为0,那么为重入锁,更新当前线程的锁的状态。返回false,没有真正的释放锁。
setState(c);
return free;
}
如果tryRelease(arg) 返回false,那么释放锁结束。如果返回true,则执行if里面的代码块。具体是 unparkSuccessor(Node node) 的逻辑:
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
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);
}
优先找下一个节点,如果下一个节点取消了,则从尾节点向前找,找到最前面一个可用的节点,然后将该节点的阻塞状态取消。在acquireQueued阻塞的线程唤醒之后继续执行。
到此,AQS简单的获取锁和释放锁的流程基本梳理完了。加深了一些对AQS的理解,不至于后续想起来或者被问起来啥也不知道。。。