该文逻辑 不是很清晰,不建议浏览,建议看这一篇;
AQS基础结构
以下内容是自己学习AQS的一些总结,如果有不同见解的,欢迎评论一起探讨,如果有理解不到位的,希望能够帮我指正。AQS的代码设计真的很精巧,可能一个判断就已经包含了n多种含义。但理解了它,会让自己对并发的理解更上一个层次。
首先,我们来看一张AQS的同步队列结构图:
state是一个锁标志位,是能否上锁,是否排队等所依托的一个重要属性。不同的锁对于该标志的实现也是有所区别的,例如ReentrantLock和ReentrantReadWriteLock;
Node是整个AQS的其中一个关键类,封装了队列中节点的关键信息,我们这里可以先只关注prev和next两个引用属性;prev是引用了前一个节点,next是引用了下一个队列节点,简而言之,AQS的同步队列就是利用双向链表构建的一个队列。
上锁过程涉及到的方法
相应的个人理解都写在代码注释上。
acquire(int arg) 是lock()实际调用的方法
public final void acquire(int arg) {
//tryAcquire尝试获取锁,如果获取不到则会进入acquireQueued进行排队
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt(); // 设置中断位
}
AQS的tryAcquire是由子类去实现的,这里我们以ReentrantLock的公平锁的tryAcquire(arg)为例
/**
* 公平版本的tryAcquire().
* 除非重入、没有等待者,处于第一个waiter这三种情况,否则不授予访问权限。
* 逻辑:
* 1、获取当前线程和当前队列的计数器
* 2、如果锁的状态是0,则属于自由状态
* 如果锁的状态非0,但是exclusiveOwnerThread是当前线程,则属于重入状态
* 两个都不是,排队去吧....
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// c==0代表此时的锁为自由状态
if (c == 0) {
// 判断是否需要排队,因为虽然c==0,但是有可能存在一个线程在当前线程之前就已经走到了这一步
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 处理重入
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
tryAcquire在state=0时的处理
先从hasQueuedPredecessors()入手
public final boolean hasQueuedPredecessors() {
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
/**
h!=t会分为三种情况:
1、如果h==t,则可能说明队列没有初始化,返回false,所以取反后为true,tryAcquire()也会走cas尝试获取锁
2、如果h!=t(非一个节点),此时s是队列当中第一个排队节点
例如买车票,你如果是第一个这个时候售票员已经在给你服务了,你不算排队,你后面的才算排队;
h作为第一个节点,也分为两种情况,虚拟或者持有锁的节点
如果s为空,则队列只有一个节点,因为如果大于1个节点,则肯定不存在h.next == null的情况;
如果队列长度是大于1的,则s==null不成立,此时为||,则会继续判断是否s节点的线程和当前的执行线程是否为同一个
s.thread != Thread.currentThread()又分为两种情况:
如果为true,则并非同一个线程,则整个方法执行结果为true,需要排队;
此时的情况就是队列不为空,(h.next)有人在排队,新来了一个人,这个人和排队的不是一个人,所以继续排队
如果为false,则为同一个线程,整个方法返回false,也就是不需要排队了
整体的意思是,如果当前队列元素大于1,说明有人在排队,当前线程在进入排队前会去验证第一个排队的线程是不是自己,
如果是的话,则直接进行尝试获取锁,因为第一个等待线程是处于park状态的,所以不可能说会有重入,因为重入是首先
线程是已经处于获取锁的状态;
3、队列只有一个节点,头尾指向同一个;按照AQS的原理,则这个节点应该是虚拟节点,即当前执行线程;整个方法返回false;
所以此时是不需要排队的,因为会通过自旋获取锁,获取不到才会进入排队并park;
*/
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
当hasQueuedPredecessors()返回false,取反后则进入compareAndSetState
设置锁状态,当设置成功后,再接着将exclusiveOwnerThread
设置为自身,并返回true,而如果设置失败,则返回false。
tryAcquire在当前线程为执行线程时的处理
以下这段代码是在上面tryAcquire()方法中的。
else if (current == getExclusiveOwnerThread()) {
//如果当前线程为执行线程,则先拿到进行处理重入后的state
int nextc = c + acquires;
// 如果state小于0,则报错
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
// 否则设置state并返回true
setState(nextc);
return true;
}
如果tryAcquire返回false,即尝试获取锁失败,则会走acquire()方法中后续的acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
这两个方法。
/**
* 通过当前线程和给定的模式创建节点并且入队
* @param mode 独占模式(Node.EXCLUSIVE),共享模式(Node.SHARED)
* @return 返回新节点
*/
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
//获取尾节点
Node pred = tail;
// 如果尾节点不等于空,则说明队列已经初始化了,则有如下操作
// 1、将新节点的prev设置位原先的尾节点
// 2、CAS将新节点设置为队列的尾节点
// 3、如果CAS成功,将原先的尾节点的next设置为新节点,返回新节点
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 队列未初始化,或者新节点进队失败则会进行enq()操作
// 至于为什么会失败,有可能是在某一瞬间存在t1和t2都在往队列中addWaiter,如果t1先入队成功,则此时tail节点已经
// 改变了,所以CAS失败
enq(node);
return node;
}
enq(final Node node) 方法带有循环操作,请注意查看循环体
/**
* 插入节点到队列中,如果有必要则进行队列初始化
* 1、进入死循环自旋,记住这里是死循环
* 2、获取尾节点
* 如果尾节点为空,则队列未初始化,新建一个执行节点
* 如果队尾节点不为空,则说明队列已经初始化了,直接入队即可
* 3、将新节点的prev引用指向队尾节点,再用CAS 的方式将当前节点设置为尾节点,再将原先的尾节点的下一个节点设置为新节点
* 4、返回当前节点位置的前一个节点
* @return 节点的前一个节点
*/
private Node enq(final Node node) {
// 如果队列为空,则创建一个占位头节点,该节点Thread为空
// 然后重新执行第二遍循环,将新队节点加入队列的第二个节点
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 如果队列本来就不为空,则将新节点的前一个节点的引用设置为原先的尾节点
// 并通过CAS将新节点设置为尾节点,返回队列未修改前的那个尾节点,这个返回没什么实际意义
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
acquireQueued(final Node node, int arg)用于设置线程休眠,并对处于队列第一个等待节点的节点进行尝试获取锁操作,该方法带有自旋操作。
/**
* @param node 按照原先的逻辑,这里的参数就是在addWaiter中创建的那个新节点
* @param arg the acquire argument
* @return 返回true的话则说明中途被中断过
*/
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true; // 标记是否竞争锁失败
try {
boolean interrupted = false; // 标记是否等待过程中被中断过,用于最后的自我中断
// 请记住,这里是一个死循环,死循环,死循环操作
// 如果线程在这个循环里park()了,下次醒来还会重新循环操作一遍
for (;;) {
// 返回上一个节点, 为空时抛出NPE
final Node p = node.predecessor();
// 这里要先记住节点是已经存在于队列中了,已经存在于队列中了,已经存在于队列中了
// 如果刚才入队的节点的前一个节点是头节点,则入队的节点应该是队列中第一个
// 在进队之后,节点是第一个等待节点,则还会进行一次尝试获取锁操作
// 与之前的tryAcquire的含义不同,这里是表示进队之后如果是第一个节点,
// 就再看一下获取锁的线程节点是否已经release,如果已经release了,就不休眠了,直接获取锁
// 而如果当前节点的前一个节点并非头节点,则说明当前节点前面节点还没执行,怎么也轮不到当前节点获取锁啊...
if (p == head && tryAcquire(arg)) {
// setHead主要就是将目标节点设置为头节点,将prev,thread设置为空
setHead(node);
// 此时是新节点成为了头节点,所以后续无节点
p.next = null; // help GC
failed = false;
return interrupted;
}
// 如果此时的前一个节点并非头节点,则也不存在抢锁的必要,因为前面的都还没执行,哪轮得到新节点
// 同时,如果前一个节点是头节点,但是抢锁失败也会来到这里,置于为什么会枪锁失败,
// 还是因为可能会有一个线程先后的问题
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
shouldParkAfterFailedAcquire方法主要有两个作用,一个是对于已取消节点的整理,方便GC回收;另一个是对于节点间信号的控制;同时,当且仅当目标节点的前一个节点的ws状态已经为SIGNAL时才回返回true。
/**
* 作用1、节点整理,跳过取消节点后将新节点设置到第一个正常节点后面
* 作用2、信号控制
* @param pred node's predecessor holding status
* @param node the node
* @return 除非前一个节点的ws状态为SIGNAL,不然不会返回true
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus; // 前一个节点的状态
// SIGNAL(-1):表示后继结点在等待当前结点唤醒。
// 后继结点入队时,会将前继结点的状态更新为SIGNAL
// 如果前一个节点是SIGNAL,已经设置为后继节点在等待前置节点唤醒的状态的话,就安心休息了
if (ws == Node.SIGNAL)
return true;
// 如果状态大于0,则处于CANCELLED状态,说明前一个节点处于取消状态
// 那么此时节点不应该放在取消节点后啊,
// 就好比,排队买票,有人排到一半中途出去吃饭,那后面的人肯定就往前挪,不可能给他保留位置等他吃饭回来再继续往前走
if (ws > 0) {
// 先记住,node本来是队列的最后一个节点,同时它的next=null
// 如果前驱放弃了,那就一直往前找,直到找到最近一个正常等待的状态,并排在它的后边。
// 那些放弃的结点,由于被node“加塞”到它们前边,它们相当于形成一个无引用链,稍后就会被保安大叔赶走了(GC回收)!
// 原队列 [n1]<-->[n2]<-->[n3]<-->[n4]-->null,若n3和被取消,则队列变成 [n1]<-->[n2]<-->[n4]-->null
do {
// 原本的代码是: node.prev = pred = pred.prev; 相当于下面的代码
pred = pred.prev;
node.prev = pred;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 前一个节点必须为0或者传播状态,将前置节点设置为SIGNAL后还不能park,
// 需要重试以确定前置节点真的被设置成了SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
parkAndCheckInterrupt()这个其实就是调用了unsafe的park()使线程进入睡眠,当然,如果线程被重新唤醒,那也是从这个位置开始继续执行
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); // 最终休眠位置
return Thread.interrupted(); // 返回线程的中断状态
}
总结
请牢记,这里的tryAcquire是用ReentrantLock的FairSync的tryAcquire进行介绍的。不同的锁在这方面的实现是不同的,相对来说,ReentrantLock的FairSync提供的比较好理解。
到这里,对整体的执行逻辑进行流程总结,详细逻辑点还是需要查看上面的源码解析:
acquire到addWaiter的整体流程
从addWaiter到acquireQueued的整体流程
简而言之就是:
- lock会调用acquire方法去进行获取锁。
- acquire方法会涉及到判断是否队列有人(公平情况下),如果有人的话是否我是排队的第一个,如果是的话就先不睡了(详细情况看上面),而当真的竞争不到锁,则将自己加入到队列中。
- 再由acquireQueued去判断需不需要休眠,如果此时是第一个等待节点,那就再去竞争锁。这里是有一个自旋操作的,当需要休眠并且确定前一个节点已经设置SIGNAL了就去睡觉。最终的休眠是调用UnSafe.park()实现的。
在从addWaiter到acquireQueued的整体流程这张图中还涉及到另一个方法,cancelAcquire(),实际上这个方法设计到了AQS同步队列的出队操作,有关这方面的理解我会留到第二篇博客中介绍。