文章目录
一直对AQS认识的比较浅,工作之余希望开始整理一下对此处源码的阅读心得。
AQS整体理解
AQS:AbstractQueuedSyncrhonizer,抽象的基于队列的同步器
首先这是一个抽象类,其作用是作为一个同步器来给很多不同功能的 Lock 实现提供 对共享资源的 同步访问。
AQS内部使用 state 变量来保存共享资源的 数量。
例如 ReentrantLock中,其中lock()的实现,就是在内部先 实现了一个AQS的实现类Sync,调用了Sync的lock()方法, 而Sync的lock() 则是通过调用 AQS模板类中的 acquire()方法来操作 共享变量,从而实现锁的逻辑。
简单的理解state作为共享变量如何体现锁的概念?
对独占锁来说,可以理解为共享变量是一个全局唯一的,所以这时使用state=0来代表锁资源m还没被占用,state=1代表已经被占用;通过casState(0,1)来尝试加锁,casState(1,0)来尝试解锁
而不同的Lock实现,如ReentrantLock,Semaphore等,对于共享变量的设置都是不同的,有的是独占的,state最大为1,有的是资源池形式的,state可以等于n;
所以对于cas最终要acquire多少个资源,以及一些公平和非公平逻辑,这个操作和判断是留给Lock中 AQS的实现类Sync中定义的,也就是tryAcquire()方法,这个方法在acquire()会被调用,来完成对 state的操作;
而acquire()作为模板方法,用来实现等待队列的构建,以及线程的阻塞等
AQS的主要方法和业务目标
public final void acquire(int arg)
//负责调用tryAcquire(arg),获取失败的话,则进入等待队列
//此方法无法响应中断,如果出现中断,则返回中断标识,然后再重新调用interrupt()方法一次,来设置中断标识,避免AQS处理逻辑丢失了中断标识位,而恰好外部线程想判断中断标识位
public final void acquireInterruptibly(int arg)
//主要逻辑类似,只不过这个可以抛出中断异常
AQS基本框架
- 上图中有颜色的为
Method
,无颜色的为Attribution
。 - 总的来说,AQS框架共分为五层,自上而下由浅入深,从AQS对外暴露的API到底层基础数据。
- 当有自定义同步器接入时,只需重写第一层所需要的部分方法即可,不需要关注底层具体的实现流程。当自定义同步器进行加锁或者解锁操作时,
先经过第一层的API进入AQS内部方法,然后经过第二层进行锁的获取,接着对于获取锁失败的流程,进入第三层和第四层的等待队列处理,而这些处理方式均依赖于第五层的基础数据提供层。
原理概览
AQS核心思想是,如果被请求的共享资源空闲,那么就将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态;如果共享资源被占用,就需要一定的阻塞等待唤醒机制
来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中。
CLH:Craig、Landin and Hagersten队列,是单向链表,AQS中的队列是CLH变体的虚拟双向队列(FIFO),AQS是通过将每条请求共享资源的线程封装成一个节点来实现锁的分配。
AQS使用一个Volatile的int类型的成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作,通过CAS完成对State值的修改
。
数据结构
AQS中最基本的数据结构——Node
,Node
即为上面CLH变体队列
中的节点
。
state
AQS中维护了一个名为state的字段,意为同步状态,是由Volatile修饰的,用于展示当前临界资源的获锁情况。
// java.util.concurrent.locks.AbstractQueuedSynchronizer
private volatile int state;
其中如果是第一次争抢state,是使用cas来争抢,如果是重入锁进行state设置,则直接set即可
这几个方法都是Final
修饰的,说明子类中无法重写它们。我们可以通过修改State字段表示的同步状态来实现多线程的独占模式和共享模式(加锁过程)
。
独占模式加锁流程
共享模式加锁流程
对于我们自定义的同步工具,需要自定义获取同步状态和释放状态的方式,也就是AQS架构图中的第一层:API层。(通常是 重写 tryAcquire()方法,来修改当前 同步器对于获取state的一些自定义逻辑,比如是否公平,是否支持重入等
)
AQS重要方法与ReentrantLock的关联
从架构图中可以得知,AQS提供了大量用于自定义同步器实现的Protected方法。自定义同步器实现的相关方法也只是为了通过修改State字段来实现多线程的独占模式或者共享模式。自定义同步器需要实现以下方法(ReentrantLock需要实现的方法如下,并不是全部
):
方法名 | 描述 |
---|---|
protected boolean isHeldExclusively() | 该线程是否正在独占资源。只有用到Condition才需要去实现它。 |
protected boolean tryAcquire(int arg) | 独占方式。arg为获取锁的次数,尝试获取资源,成功则返回True,失败则返回False。 |
protected boolean tryRelease(int arg) | 独占方式。arg为释放锁的次数,尝试释放资源,成功则返回True,失败则返回False。 |
protected int tryAcquireShared(int arg) | 共享方式。arg为获取锁的次数,尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。 |
protected boolean tryReleaseShared(int arg) | 共享方式。arg为释放锁的次数,尝试释放资源,如果释放后允许唤醒后续等待结点返回True,否则返回False。 |
一般来说,自定义同步器要么是独占方式,要么是共享方式
,它们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared
中的一种
即可。AQS也支持自定义同步器同时实现独占和共享两种方式
,如ReentrantReadWriteLock。ReentrantLock是独占锁,所以实现了tryAcquire-tryRelease
。
以非公平锁为例,这里主要阐述一下非公平锁与AQS之间方法的关联之处,具体每一处核心方法的作用会在文章后面详细进行阐述。
加锁:
- 通过ReentrantLock的加锁方法lock()进行加锁操作。
- 会调用到内部类Sync的lock()方法,由于Sync#lock是抽象方法,根据ReentrantLock初始化选择的公平锁和非公平锁,执行相关内部类的lock()方法,本质上都会执行AQS的acquire()方法。
- AQS的acquire()方法会执行tryAcquire方法,但是由于tryAcquire需要自定义同步器实现,因此执行了ReentrantLock中的tryAcquire方法,
由于ReentrantLock是通过公平锁和非公平锁内部类实现的tryAcquire方法,因此会根据锁类型不同,执行不同的tryAcquire
。 - tryAcquire是获取锁逻辑,获取失败后,会执行框架AQS的后续逻辑,跟ReentrantLock自定义同步器无关。
解锁:
- 通过ReentrantLock的解锁方法unlock()进行解锁。
- unlock()会调用内部类Sync的release()方法,该方法继承于AQS。
- release()中会调用tryRelease方法,tryRelease需要自定义同步器实现,tryRelease只在ReentrantLock中的Sync实现,
因此可以看出,释放锁的过程,并不区分是否为公平锁
。 - 释放成功后,所有处理由AQS框架完成,与自定义同步器无关。
通过上面的描述,大概可以总结出ReentrantLock加锁解锁时API层核心方法的映射关系
。
ReentrantLock 为例分析加锁和解锁流程
lock()
1、由于这是一个显示锁Lock接口的实现,所以从lock()方法进入加锁
public void lock() {
sync.lock();
}
可以发现调用了内部Sync对象的lock()方法
//abstract static class Sync extends AbstractQueuedSynchronizer
abstract void lock();
2、内部的Sync类也是抽象的,而lock()方法也是抽象的,所以lock应该在实现类中实现
3、分别来看两个不同的Sync实现
//static final class NonfairSync extends Sync
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
//一上来就直接先尝试修改state,成功的话则表示自己成功获取Lock,然后将排他线程属性设置为当前线程
//这其实也就是不排队的方式,一上来就去抢锁,不判断队列情况,所以很可能在抢在队列中等待的线程之前拿到锁
//如果直接cas抢不过来,则使用acquire(1),试图使用AQS中等待队列的方式,看看是排队呢还是自旋
//static final class FairSync extends Sync
final void lock() {
acquire(1);
}
//公平锁则不会直接抢夺,直接使用acquire(1),直接使用AQS队列,看看需不需要构造队列,如果不需要则自旋
4、AQS的acquire()方法实现:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
4.1、首先会调用子类实现的tryAcquire(arg)
,如果不成功,则尝试进入等待队列
//ReentrantLock中实现了FairSync和NonFairSync,分别实现了不同功能的tryAcquire(int arg)
//公平锁
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) { //首先判断当前state是否为0,为0才有直接获取锁的可能
if (!hasQueuedPredecessors() && //公平锁要保证公平,就是尽可能保证先来的线程先拿锁,所以不能直接进行cas,要先看看等待队列中是否有 已经在等待的前辈们
compareAndSetState(0, acquires)) { //!hasQueuedPredecessors() 成立也就是hasQueuedPredecessors()返回false,代表没有前辈线程在等待,则尝试cas,如果cas也成功了
setExclusiveOwnerThread(current); //则设置排他线程属性为当前线程
//这说明了当没有线程等待时,可以直接占有锁,也不需要管queue
return true;
}
}
//如果c != 0,则说明已经有线程正在使用锁,那么就先看看 是不是当前线程在重入,也就是当前线程重复获得锁。这里支持重入。
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc); //将state属性进行 累加
return true;
}
return false;
}//从这里也可以看出,state只存在两种可能, 0 或者一个正数,由于是独占锁支持重入,正数可能>1,但是都必须是同一个线程给占用的state,也就是state>1的时候,state的值 必须是同一个线程累加上去的
//非公平锁,调用了父类Sync中的 nonfairTryAcquire()方法,也就是说Sync默认也是使用非公平的,所以将非公平的实现直接写在了 Sync这个抽象类中。
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(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;
}
注意,这里一定是首先判断state==0,如果state已经被别人占用了,则就无从谈起是否要判断hasQueuedPredecessors()
所以下面hasQueuedPredecessors()判断的前提,也是当前state还没有被占用
4.2 公平锁中hasQueuedPredecessors() 判断是否有等待的前辈,返回true则代表有等待的,如果返回true,那么公平锁的 tryAcquire()就要返回false
//AQS提供了判断是否有前驱等待线程的方法
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
-
先判断
h != t
,这里如果返回false,也就是h=t,则直接短路返回false; h=t 代表当前根本没有初始化等待队列,也就说明在此刻并没有在队列中等待的线程
(这里判断没有等待的,只是给了当前线程可以cas的机会,但是不代表cas一定成功,因为并发随时可能发生,可能有多个线程同时判断了等待队列为空,同时cas,那么只有一个cas可以成功) -
h != t 通过了,说明h!=t,
则说明等待队列已经存在了
;那么就进行后半部分,也就是((s = h.next) == null || s.thread != Thread.currentThread())
-
这是一个 或运算,前半部分 (s = h.next) == null 如果为 true,则直接短路返回,这里为true的情况可能有:
-
此时已经有别的线程(线程b)cas成功抢到锁了,而此时还有一个线程c cas失败了,开始进入了构造queue的过程,那么就可能在初始化queue的时候,导致h.next=null(这里要结合后续的enq(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)) { //在这个位置,如果是初始化完毕后,并且cas成功,那么当前head指向new Node(),tail指向传入的node,当前栈上t仍然指向head,在t.next=node之前,目前的指针缺失了head.next,此时,虽然队列已经开始初始化了,但是还差最后一步,将上一个节点(在初始化时上一个节点就是头结点,封装了一个new Node)的next指向 新加入的node t.next = node; return t; } } } }
-
基于上面的代码,则(s = h.next) == null 如果为 true,很可能是其他线程已经开始enq方法,也就是开始进入构造等待队列的过程中,此时很可能有一个短暂的中间状态,虽然h != t 也就是虽然队列已经构造除了 head 和tail,并且head != tail,但是还可能差最后一步 将head.next ->node;在这个时刻可能造成 h.next == null
-
所以这个判断解读为: 如果h!=t(已经有队列,或者至少保证已经开始构造队列了),但是如果 (s = h.next) == null 如果为 true,说明此时有
其他线程已经enq成功(肯定不是自己,因为如果是自己的话在当前线程不会出现这种中间状态的临时判断,这里h.next 肯定不是Null)
,则就代表有前驱线程
-
-
h != t && ((s = h.next) == null || s.thread != Thread.currentThread())
最后一个判断s.thread != Thread.currentThread())
较好理解,就是如果有队列了,但是 (s = h.next) == null 为false(代表确实已经有等待的线程了(可能是其他线程,也可能是自己这个线程),而且这个线程在enq的完毕了,已经将双向链表维护好了指向关系,所以这里看到的才 不是null
),才会走到最后一个判断,这里就是判断等待的线程是不是自己;为何要进行这个判断?是因为在并发不高的场景下:比如虽然当前锁被占了,但是只有自己线程去尝试获得锁,那么可能对当前线程进行一定的自旋等待,来重复的去cas尝试;所以 判断能走到这里并且s.thread != Thread.currentThread())
如果是true,代表当前线程不是 老二,则肯定有前驱的其他线程在等待,如果判断走到这里,但是这里返回false,则造成这个现象的原因就是 当前线程在 acquireQueue方法中,一直自旋来尝试获得锁,所以acquireQueue会一直调用tryAcquire,从而一直判断是否有前驱,只要锁还没被释放,那么当前线程就一直会走这个s.thread != Thread.currentThread())
判断,意思就是:等待队列虽然有了,但是是当前线程作为 下一个继任者一直在等,所以对当前线程来说,是没有前辈在等待的,只有一个正在执行的线程占有锁罢了。
所以总体来说,hasQueuedPredecessors()返回true(有前驱线程)的可能性有:
- 等待队列非空(h != t成立),并且
(s = h.next) == null
成立 或者s.thread != Thread.currentThread()成
;- 也就是 如果等待队列不空,如果头节点的下一个节点是null,则说明肯定有人正在进行enq()方法的初始阶段,所以这个线程肯定比当前线程早,所以前驱线程判断成立
- 也可能 等待队列不空,而且 头结点.next 也不是null,但是头结点的写一个节点所代表的线程也不是当前线程,那更说明有前驱线程
hasQueuedPredecessors()返回false(没有前驱线程)的可能性有:
- h == t,则直接短路返回false,因为压根还没有等待队列,所以至少当前没有线程在等,所以线程即使并发,也应该是公平的进行cas,让总线仲裁。
- h != t ,就是有等待队列了, 则
((s = h.next) == null || s.thread != Thread.currentThread())
要为false,则两个子条件都为false;也就是说(s = h.next) != null
,第二个节点不是null,但是s.thread == Thread.currentThread()
成立,而当前线程就是头节点的下一个等待者,也就是当前线程就是马上要执行的线程,那么就说明没有线程排在前面
tips:双向链表中,第一个节点为虚节点,其实并不存储任何信息,只是占位。真正的第一个有数据的节点,是在第二个节点开始的。在acquireQueued()方法中,如果当前节点cas获取锁成功,则调用setHead(node)方法,让head->node,但是这只是让当前node来做这个虚节点
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
//setHead方法是把当前节点置为虚节点,但并没有修改waitStatus,因为它是一直需要用的数据
acquireQueued() 和 addWaiter()
前面从 ReentrantLock的 lock()开始,分析了其内部 FairSync和 NonFairSync的lock()实现以及tryAcquire()实现
lock() --->AQS.acquire()--->子类.tryAcquire() //在公平锁内部,特殊的分析了hasQueuedPredecessors()
下面继续分析acquire()方法中的acquireQueued()和addWaiter()
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
} //构造了一个Node对象来代表当前和保存thread,nextWaiter属性用于condition的等待队列,这里没用到condition,所以传进来的是null
如果tryAcquire(arg) 返回true,即代表获取锁成功,则!tryAcquire(arg)为 false直接短路
如果tryAcquire(arg) 成立,则!tryAcquire(arg)为true,意味着 如果获取锁不成功,则进行下面的操作(入队)
static final class Node {
static final Node SHARED = new Node();
/** Marker to indicate a node is waiting in exclusive mode */
static final Node EXCLUSIVE = null;
/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED = 1;
/** waitStatus value to indicate successor's thread needs unparking */
static final int SIGNAL = -1;
/** waitStatus value to indicate thread is waiting on condition */
static final int CONDITION = -2;
/**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate
*/
static final int PROPAGATE = -3;
}
//内部类Node中定义了几个静态变量,其中这里使用的Node.EXCLUSIVE 为null
从内向外,先分析addWaiter(Node mode)方法
/**
* Creates and enqueues node for current thread and given mode.
* 使用给定的模式 来为当前线程创建一个node,并加入队列,Node.EXCLUSIVE代表独占锁
* @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
* @return the new node
*/
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail; //先拿到tail,放到栈中的局部变量表
if (pred != null) { //如果tail 非空,则代表已经有队列了
node.prev = pred;
if (compareAndSetTail(pred, node)) { //cas失败,则说明有并发线程在入队
pred.next = node;
return node;
}
}
enq(node); //如果刚才拿出的tail 为null,或者 cas设置新的tail失败,则使用enq来加入队列
return node;
}
//无限循环直到代表 当前thread的node加入queue,如果需要的话,则初始化queue
private Node enq(final Node node) {
for (;;) {
Node t = tail; //先拿出tail。如果tail是空,则说明还没有队列,则尝试cas 设置head为一个new Node()
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
//cas成功之后,则暂时先让tail=head=new Node()
//这一步如果并发,那么就只有一个线程cas成功,失败的线程再次for循环
tail = head;
} else { //第二次for,或者一进来就发现tail !=null
node.prev = t; //则先把当前node的 prev指向t,也就是保存下来的tail
//注意这里指的是 保存下来的tail而不一定是 当前真正的tail节点,因为并发随时存在,tail一直可能会变
//然后尝试将tail设为当前node,这里cas成功,则说明从进入enq,并将t=tail保存下来之后,都没有人来修改过tail,如果失败,则需要重新获取tail,说明有的线程先来一步,已经将tail指向了自己。
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
对addWaiter(),和enq方法总结就是
- addWaiter()首先判断一次tail是不是为空,并尝试cas设置 tail 为当前node,如果设置成功,则链表构建完毕
- 如果addWaiter()前半部分逻辑判断失败,则进入enq
- enq首先仍然 先把tail 保存到栈帧局部变量t中,然后判断t是不是为空来看是否需要初始化队列
- 如果需要初始化队列,则尝试set head= new Node() 所以初始化队列时不是将head指向当前线程节点,如果成功,则将tail = head ,也就是此时tail 和head都指向一个new Node();这个Node是没有意义的一个Node作为头结点
- 如果cas set head失败,或者set成功,则进入下一次for循环,仍然拿出tail来保存到t(每次循环都要重新拿一次tail,因为并发随时发生,t保存的tail,可能就不是真正的tail),然后尝试cas set tail= 当前线程Node ,如果成功,则当前线程入队成功,否则再次for循环,直到当前线程cas set tail 指向 当前Node未知
addWaiter
和enq
方法中新增一个节点时为什么要先将新节点的prev置为tail再尝试CAS
,而不是CAS成功后来构造节点之间的双向链接
?
这是因为,双向链表目前没有基于CAS原子插入的手段,如果我们将node.prev = t和t.next = node(t为方法执行时读到的tail,引用封闭在栈上)
放到compareAndSetTail(t, node)成功后执行,如下所示:
if (compareAndSetTail(t, node)) {
node.prev = t;
t.next = node;
return t;
}
会导致这一瞬间的tail也就是t的prev为null,这就使得这一瞬间队列处于一种不一致的中间状态。
有如下情景:将原来的tail节点定义为nodeTail
- 时刻1:tail=t 都指向了nodeTail,线程A进入,进行了如上的CAS,将tail指向了node,然后马上cpu切换到线程B
- 时刻2,线程B进入cpu,获取t=tail ,这时线程b的tail指向的是node,但是由于线程a里没有设置node的prev,所以此时tail的prev=null,也就是短暂的队列不一致。
- 所以将node.prev=t放在cas之前,但是这样会不会导致node的prev会出错呢?
- 其实这样最多只会造成
当前这个线程的node
可能在一小段时间内。prev指向的不是真正的tail。(多并发下,tail常常变。) - 其实不会,如果CAS成功,其实prev确实指向了上一个tail,那么tail.next也就指向node
- 如果cas失败,则自旋,会一直循环更新node.prev,直到CAS成功指向某一时刻的tail,tail.next也指向node,从而保持双向一致。
- 其实在node.prev=t放在cas之前,则保证了多并发场景下,维持链表上的数据最低的一致(也就是不会出现上述在一瞬间prev=null,使得链表断掉这种情况)。
保证后续从tail开始从后向前prev遍历的时候,链表可用,不出现prev=null的状态
。其实也就是,只要tail cas 设置成功了,则说明enq的一次for循环中,tail还没有并发被修改,所以当前线程设置的prev一定是有效的,也就是originTail节点,所以这个链表从后向前必定是有效的;保证了从tail开始遍历的话,肯定是可以遍历完整个链表,而且由于tail设置之前prev就有了,所以prev一定能保证链表完整 - 这也解释了为何
unparkSuccessor
使用prev来遍历链表。
//这里传入的node,就是addWaiter()返回的Node对象,也就是当前线程的Node
//arg 一般为1,代表当前获取资源的数目
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) { //for循环来保证自旋,在线程醒来后,仍然for循环尝试获取锁
final Node p = node.predecessor(); //获取前驱
if (p == head && tryAcquire(arg)) { //如果当前节点就是head的下一个,则说明有资格获取锁,则允许tryAcquire();
//这种场景可能是并发较少,比如当前线程刚进来,只有一个线程正在执行,就是头结点所在的线程
//也可能是头结点线程执行完了,把当前线程唤醒,然后当前线程来tryAcquire一次就成功了
//成功之后来setHead
setHead(node);
p.next = null; // help GC,将原来的head从链表中拆除
failed = false;
return interrupted; //返回中断状态,因为当前方法不支持中断异常
}
//如果上一个if失败,则判断是否应该park,并且检查中断状态
//如果这里返回false,则重新进入for循环,判断p == head && tryAcquire(arg),不成功的话再判断shouldParkAfterFailedAcquire,第二次shouldParkAfterFailedAcquire 通常就会返回true了,因为前一个Node已经被我们设为了Signal
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//这里如果parkAndCheckInterrupt()返回中断状态,则将这个状态返回出去
interrupted = true;
}
//出任何异常的话,则退出竞争
} finally {
if (failed)
cancelAcquire(node);
}
}
//pred是前驱节点,node是当前节点
//这里pred有可能就是head,当前节点由于虽然是head.next,但是其cas失败了,所以也应该进入此方法进行判断,通常这时候head节点很可能不是signal,所以当前node还有一次机会也就是这里casSetHead Ws=Signl后返回false,重新cas尝试一次
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
//如果前驱节点状态就是SIGNAL,说明前驱都处于 等待通知的状况,那么当前node就老实排队
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) { //前驱节点处于cancel
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do { //将node前驱指向 pred的前驱,意思就是将pred从队列中拿掉
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0); //一直循环,拿掉那些cancel状态的Node,直到碰到一个有效的Node停止。
//拿掉一部分cancel Node后。注意可能只是一部分,因为cancel Node可能不连续
//找到一个有效的prev后,构建双向链表
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.
*/
//如果前驱节点既不是Signal,也不是cancel,那么就只能是0或者propagate,可能前一个节点就是0,那么就把前一个节点设为Signal,让他处于等待signal的状态,自己则返回false,重新在试一次看看能不能满足 p=head并且tryAcquire()成功,如此往复
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
//执行park,如果park醒来,则返回中断状态,并且重新回到上一层方法进行for循环,尝试获取锁
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
//最终,如果 if判断都成立了,也就说明acquireQueued 最终也从park阻塞中返回了中断状态
//虽然acquire会成功执行完毕,但是中断状态需要保留
//再次执行selfInterrupt 来将当前线程中断一次,来让当前线程对象 内部的中断状态属性设置上
//可以让外部调用者 使用这个属性进行一定的判断逻辑
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
很显然,如果只有一个线程在等,那么这个线程就是作为一个 head = new Node()的下一个节点,而这个线程所在的Node,由于没有后来者给他设置ws,所以其一直ws 一直为0
但是这也没关系,因为这个线程一直就是head.next;所以其会一直自旋,根本不用通过 SIGNAL这种标识来唤醒;
acquireQueued方法流程图为:
shouldParkAfterFailedAcquire流程:
也就是说,只要进入了shouldParkAfterFailedAcquire
,这个方法由于不响应中断,所以最多将中断保留,但是不抛出异常,所以中断了只是让线程醒来,然后马上去重新尝试获取锁,在没有别的异常发生情况下,acquire()方法一定能在 有限次 park()和唤醒之后,成功获得锁(因为不会中途被中断打断)。
p == head && tryAcquire(arg),有没有可能tryAcquire(arg)失败?
有可能,在非公平锁的情况下,有可能tryAcquire并发失败。
到此为止,ReentrantLock的lock()方法结束,一般的lock(),使用的是不响应打断的方式获取AQS state属性,中间伴随着对 AQS中等待队列的操作、遍历和前驱结点,头尾节点等判断;最终在高并发场景下保证了能够顺利获取临界资源和顺利进入等待。
unlock()
public void unlock() {
sync.release(1); //仍然调用同步器对象的方法,只不过release方法在AQS抽象类中就有实现了
}
//release(),对应acquire()
//AQS
/**
* Releases in exclusive mode. Implemented by unblocking one or
* more threads if {@link #tryRelease} returns true.
* This method can be used to implement method {@link Lock#unlock}.
* 以独占锁形式释放资源,如果tryRelease返回true,则其实现就是通过解锁一个或多个线程
* @param arg the release argument. This value is conveyed to
* {@link #tryRelease} but is otherwise uninterpreted and
* can represent anything you like.
* @return the value returned from {@link #tryRelease}
*/
public final boolean release(int arg) {
if (tryRelease(arg)) { //如果release了所有的资源,则进入if body
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
//Sync实现了tryRelease()
protected final boolean tryRelease(int releases) {
int c = getState() - releases; //查看当前的state和release数目之间的差值
if (Thread.currentThread() != getExclusiveOwnerThread()) //如果当前线程不是 占有锁的排他的线程,则报异常
throw new IllegalMonitorStateException();
//如果走到这,说明当前线程就是 独占锁线程,所以可以执行以下具体的state修改操作
boolean free = false;
if (c == 0) { //如果这次release完了,剩余的资源数为0,则表示全部释放
free = true; //表示释放完毕
setExclusiveOwnerThread(null); //exclusiveOwnerThread对象改为null
}
setState(c); //不管是否释放完,都要修改状态,因为可以重入,所以应该就可以release多次,支持一次release一部分,或者一次 -1这种操作。
return free;
}
对 release(int arg)
方法中 if (h != null && h.waitStatus != 0)
对这个判断的解读:
- h != null,如果这个不满足,即h==null,则直接短路,不进行后续节点的唤醒;因为等待队列都没有,就没必要唤醒
- h != null满足了,再看
h.waitStatus != 0
,首先要知道 什么时候h.waitStatus == 0
, 等于0只可能出现在队列最开始的情况,或者是竞争极为不激烈,没有出现队列中排队数 >2的情形(大于2的话,队列中第二个等待的线程会在park()之前 将前驱设为Signal,所以前驱如果获得锁并 成为head,那么他的ws一旦为Signal,那么后继一定是有人的) - 所以如果
h.waitStatus == 0
,所以一定是竞争不激烈,只有一个线程等待锁,那么这个等待线程也就不需要唤醒,因为其会尝试shouldParkAfterFailedAcquire
两次(第一次将pred改为signal,第二次才会park),所以这里ws=0说明 后继线程还在run呢,还没有到park的时候,所以不必要唤醒;只有当前h.waitStatus != 0
成立,说明当前head代表的node肯定是被后继给标记为SIGNAL了,而且后继很有可能在标记 前驱为SIGNAL之后就park()了;所以此时应该进入unparkSucessor()的逻辑;
//传入的node,一般为上一步 保留在栈帧中的 h 变量,也就是当前的head
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.
*/
//如果当前是signal,则将当前头节点 ws设为0
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.
*/
//先找h.next,如果是null,或者 处于Cancel状态
//s==null 很可能出现在enq()方法、或者在产生CANCELLED状态节点的时候,先断开的是Next指针,Prev指针并未断开,因此也是必须要从后往前遍历才能够遍历完全部的Node。
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
//一路从tail向前找到最前面的 t.waitStatus <= 0 的节点,当做当前head的next
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
//对找到的那个线程,使用unpark,也就是颁发一个许可。
if (s != null)
LockSupport.unpark(s.thread);
}
这里遗留了一个问题,什么时候
compareAndSetWaitStatus(node, ws, 0);
会失败呢?
cancelAcquire(Node node)
shouldParkAfterFailedAcquire()方法中,有对Cancel状态节点的判断,那么什么时候会生成CANCELLED状态节点?
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 {
//如果没有正常在上面的for循环中将failed改为false,那么就是出异常了
if (failed)
cancelAcquire(node);
}
}
// java.util.concurrent.locks.AbstractQueuedSynchronizer
private void cancelAcquire(Node node) {
// 将无效节点过滤
if (node == null)
return;
// 设置该节点不关联任何线程,也就是虚节点
node.thread = null;
Node pred = node.prev;
// 通过前驱节点,跳过取消状态的node,直到找到一个正常的前驱
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// 获取过滤后的前驱节点的后继节点
Node predNext = pred.next;
// 把当前node的状态设置为CANCELLED
node.waitStatus = Node.CANCELLED;
// 如果当前节点是尾节点,将从后往前的第一个非取消状态的节点设置为尾节点
// 更新失败的话,则进入else,如果更新成功,将tail的后继节点设置为null
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
//如果当前不是尾节点,或者更新尾结点cas失败(可能是有并发入队的线程cas抢占了)
//则说明目前不需要这里来进行 casSetTail操作,也不需要设置 prev.next为null
} else {
int ws;
// 如果当前节点不是head的后继节点,1:判断当前节点前驱节点的是否为SIGNAL,2:如果不是,则把前驱节点设置为SINGAL看是否成功
// 如果1和2中有一个为true,再判断前驱节点的线程是否为null
// 如果上述条件都满足,把当前节点的前驱节点的后继指针指向当前节点的后继节点
if (pred != head && ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && pred.thread != null) {
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
// 如果当前节点是head的后继节点,或者上述条件不满足,那就唤醒当前节点的后继节点
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
对条件 if (pred != head && ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && pred.thread != null)
的解读:
-
pred != head 代表前驱节点不是head,如果是head,则进入else,直接唤醒后继即可。因为此时node.next就是事实上的队列的第一个等待者了
-
pred != head满足,然后如下要满足:那么意思就是说,前驱节点要么已经是Signal了,要么cas 他为signal成功;已经是signal好理解,应该是pred的后继线程设置的(pred和node之间也很可能夹杂了好几个cancel状态的node),也有可能不是Signal,因为pred原来的那些后继都cancel了,还没来得及给pred设置signal;如果cas失败,则可能是目前有另一个线程也在走cancelAcquire()方法,也找到这个pred节点了,然后把他设为了signal(因为当前节点已经是cancel了,所以如果另一个线程也cancel,则很可能也找到同一个pred节点)
-
所以如果这个 || 条件不满足,则代表现在可能有并发cancel的,干脆直接unparkSuccessor,从tail开始往前找一个线程先唤醒了,让唤醒的这个线程来为前面的pred节点设置ws状态,哪怕再检查一次发现已经是signal了,再次park就是了
-
而且 || 条件不满足,直接唤醒后继线程unparkSuccessor的话,也会从tail开始遍历,将中间cancel状态的node拿掉一部分
-
pred.thread != null 表示pred是一个有意义的节点,否则=null的话(很可能是这个pred所在的线程也开始cancel了,或者pred就是head?但是进入到这了,说明pred!=head已经满足了,否则短路),则给其设置next则没意义,不如直接唤醒一个线程,让线程在unparkSuccessor流程和 acquireQueued流程中摘除这些cancel的节点
((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL)))
如果这一长串条件满足,则说明 至少在当下pred是一个正常,并且可以被signal的节点,所以给这个pred设置next=node.next(当然,这里也可能失败,毕竟还是可能并发的 排在后面的node进行cancel)
最后,要执行node.next = node;来断开next连接
其实一直看到的是,node的prev似乎一直没断开,这样应该是为了让链表一直处于可遍历的状态?这样其实也是可以gc的,即便node.prev引用了别的node,但是没有其他的node再指向这个节点,所以应该是可以gc
对cancelAcquire()方法的总结:
- 获取当前节点的前驱节点,如果前驱节点的状态是CANCELLED,那就一直往前遍历,找到第一个waitStatus <= 0的节点,将找到的Pred节点和当前Node关联,将当前Node设置为CANCELLED。
- 根据当前节点的位置,考虑以下三种情况:
(1) 当前节点是尾节点。
(2) 当前节点是Head的后继节点。
(3) 当前节点不是Head的后继节点,也不是尾节点。
根据上述第二条,我们来分析每一种情况的流程。
1、当前节点是尾节点
2、当前节点是Head的后继节点
3、当前节点不是Head的后继节点,也不是尾节点
通过上面的流程,我们对于CANCELLED节点状态的产生和变化已经有了大致的了解,但是为什么所有的变化都是对Next指针进行了操作,而没有对Prev指针进行操作呢?什么情况下会对Prev指针进行操作?
仍然是考虑极端情况的并发:
1、在当前node执行cancel时,其pred很可能也在执行cancel;所以当前node执行compareAndSetNext(pred, predNext, next);
之后,很可能pred已经变为cancelled
状态;
2、当前node.next节点很可能此时刚入队,并且进入了shouldParkAfterFailedAcquire
,然后开始向前遍历,找到一个有意义的节点,很显然,会跳过当前node,也会跳过pred
3、如果此时当前node线程,在cancelAcquire()方法中,再去同步的操作 node.next 的prev
指针,很可能将prev指针 改成指向pred;这造成了指向一个 被移除队列的Node
4、所以AQS中对链表的遍历都是通过prev,所以不会出现在多个地方并发处理prev造成不安全
5、对prev的处理,一般都在shouldParkAfterFailedAcquire
,shouldParkAfterFailedAcquire
是获取锁失败的情况下才会执行,进入该方法后,说明共享资源已被获取,当前节点之前的节点都不会出现变化(之前的节点都处于park状态),因此这个时候变更Prev指针比较安全。(此时并发较少)
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);