要研究AQS源码,需要从两个方向入手:
1.他是啥?能干啥?
2.怎么干?
第一个问题,他是啥,能干啥:
为实现依赖于先进先出 (FIFO) 等待队列的阻塞锁和相关同步器(信号量、事件,等等)提供一个框架。此类的设计目标是成为依靠单个原子 int 值来表示状态的大多数同步器的一个有用基础。子类必须定义更改此状态的受保护方法,并定义哪种状态对于此对象意味着被获取或被释放。假定这些条件之后,此类中的其他方法就可以实现所有排队和阻塞机制。子类可以维护其他状态字段,但只是为了获得同步而只追踪使用 getState()
、setState(int)
和 compareAndSetState(int, int)
方法来操作以原子方式更新的 int 值。
应该将子类定义为非公共内部帮助器类,可用它们来实现其封闭类的同步属性。类 AbstractQueuedSynchronizer 没有实现任何同步接口。而是定义了诸如 acquireInterruptibly(int)
之类的一些方法,在适当的时候可以通过具体的锁和相关同步器来调用它们,以实现其公共方法。
首先他是有一个先进先出的等待队列,该等待队列实现类似于ConcurrentLinkedQueue,该队列的目的是,当tryAcquire失败时需要将当前线程入等待队列,因此需要一定的并发准确性,也可以凭此实现公平锁和非公平锁,公平锁就是上来先不尝试tryAcuqire,而是先入队,当轮到的时候在进行acquire,非公平就是上来就tryAcquire,失败之后入队,这对其他正在等待的线程来说显然是不公平的。我们的重点也是看它实现的支持并发的队列机制,独占和共享的不同实现,通知实现
首先是AQS的公共方法列表如下:
void |
acquire(int arg) 以独占模式获取对象,忽略中断。 |
void |
acquireInterruptibly(int arg) 以独占模式获取对象,如果被中断则中止。 |
void |
acquireShared(int arg) 以共享模式获取对象,忽略中断。 |
void |
acquireSharedInterruptibly(int arg) 以共享模式获取对象,如果被中断则中止。 |
protected boolean |
compareAndSetState(int expect, int update) 如果当前状态值等于预期值,则以原子方式将同步状态设置为给定的更新值。 |
Collection |
getExclusiveQueuedThreads() 返回包含可能正以独占模式等待获取的线程 collection。 |
Thread |
getFirstQueuedThread() 返回队列中第一个(等待时间最长的)线程,如果目前没有将任何线程加入队列,则返回 null . 在此实现中,该操作是以固定时间返回的,但是,如果其他线程目前正在并发修改该队列,则可能出现循环争用。 |
Collection |
getQueuedThreads() 返回包含可能正在等待获取的线程 collection。 |
int |
getQueueLength() 返回等待获取的线程数估计值。 |
Collection |
getSharedQueuedThreads() 返回包含可能正以共享模式等待获取的线程 collection。 |
protected int |
getState() 返回同步状态的当前值。 |
Collection |
getWaitingThreads(AbstractQueuedSynchronizer.ConditionObject condition) 返回一个 collection,其中包含可能正在等待与此同步器有关的给定条件的那些线程。 |
int |
getWaitQueueLength(AbstractQueuedSynchronizer.ConditionObject condition) 返回正在等待与此同步器有关的给定条件的线程数估计值。 |
boolean |
hasContended() 查询是否其他线程也曾争着获取此同步器;也就是说,是否某个 acquire 方法已经阻塞。 |
boolean |
hasQueuedThreads() 查询是否有正在等待获取的任何线程。 |
boolean |
hasWaiters(AbstractQueuedSynchronizer.ConditionObject condition) 查询是否有线程正在等待给定的、与此同步器相关的条件。 |
protected boolean |
isHeldExclusively() 如果对于当前(正调用的)线程,同步是以独占方式进行的,则返回 true 。 |
boolean |
isQueued(Thread thread) 如果给定线程的当前已加入队列,则返回 true。 |
boolean |
owns(AbstractQueuedSynchronizer.ConditionObject condition) 查询给定的 ConditionObject 是否使用了此同步器作为其锁。 |
boolean |
release(int arg) 以独占模式释放对象。 |
boolean |
releaseShared(int arg) 以共享模式释放对象。 |
protected void |
setState(int newState) 设置同步状态的值。 |
String |
toString() 返回标识此同步器及其状态的字符串。 |
protected boolean |
tryAcquire(int arg) 试图在独占模式下获取对象状态。 |
boolean |
tryAcquireNanos(int arg, long nanosTimeout) 试图以独占模式获取对象,如果被中断则中止,如果到了给定超时时间,则会失败。 |
protected int |
tryAcquireShared(int arg) 试图在共享模式下获取对象状态。 |
boolean |
tryAcquireSharedNanos(int arg, long nanosTimeout) 试图以共享模式获取对象,如果被中断则中止,如果到了给定超时时间,则会失败。 |
protected boolean |
tryRelease(int arg) 试图设置状态来反映独占模式下的一个释放。 |
protected boolean |
tryReleaseShared(int arg) 试图设置状态来反映共享模式下的一个释放。 |
其中的五个方法是需要我们去扩展的:如下:
tryAcquire(int),tryRelease(int),tryAcquireShared(int),tryReleaseShared(int),isHeldExclusively()
首先肯定是从几个关键API方法入手,他们是:
void |
acquire(int arg) 以独占模式获取对象,忽略中断。 |
void |
acquireInterruptibly(int arg) 以独占模式获取对象,如果被中断则中止。 |
void |
acquireShared(int arg) 以共享模式获取对象,忽略中断。 |
void |
acquireSharedInterruptibly(int arg) 以共享模式获取对象,如果被中断则中止。 |
boolean |
release(int arg) 以独占模式释放对象。 |
boolean |
releaseShared(int arg) 以共享模式释放对 |
首先从acquire(int) 入手,看其是怎么实现的:
public final void acquire(long arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
//如果acquireQueued过程当前线程被打断,此时会消耗掉打断状态(Thread.interrupted()方法会消耗打断状态),此时我需 要重现该状态,因此主动调用Thread.currentThread.interrupt(),供其他方法进行判断(大多数情况下用不到,但是这么做确实体现了这段代码的严谨性)
selfInterrupt();
}
其实现也是,先tryAcuire(arg)成功了就返回,不成功就将当前线程创建一个waiter节点入队具体实现如下:
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;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
如果casTail失败,或者当前tail为空(即当前队列尚未初始化,需要进行初始化,并将当前节点重新入队,前面casTail也是入队过程,只不过失败了),以下是入队过程:
private Node enq(final Node node) {
//cas搭配for循环
//这里相对于ConcurrentHashMap依赖sizeCtl变量进行状态控制,此处代码处理的并不是很好,有大量竞争的情况下,会新建大量的
//node对象。当然代码实现上肯定是没问题的,这也是常见的代码逻辑:
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
//添加新节点要同时修改prev,next指针
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
入队完成之后,需要acquireQueued;
前一步入队只是将代表当前thread的节点插入到队列中,但是并没有开始得到signal,循环申请锁的过程,acquireQueued就是实现这个动作:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
//开一个for循环,每次release的时候都会唤醒,我需要不断检查代表当前线程的节点是否已经是
//头节点,如果是头节点,tryAcquire,不是头节点,我需要继续阻塞等待下一轮release唤醒,这是其
//主要处理逻辑。但是因为其中涉及到condition和超时取消,我需要进一步进行判断是否进行阻塞等待(通过 //LockSupport.part()实现阻塞)
//在这过程中依然会出现tryAcquire失败的情况,tryAcquire是我们自己实现的方法,一般情况下,对于shared类型, tryAcquire时会因为state
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=0,进入这个分支,将当前节点的前置节点
//waitStatus置为SIGNAL,之后返回false,结合之前的acquireQueued,将会发起新一轮for循环,如果当前节点还不是头节点或者tryAcquire失败,会再次进入这个方法,此时将会判断前置节点的waitStatus=SIGNAL,返回true,此时当前线程将会阻塞等待被唤醒。为什么是要设置当前节点的前置节点的waitStatus而不是当前节点呢?这个待会再谈
/*
* 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;
}
为什么是要设置当前节点的前置节点的waitStatus而不是当前节点呢?
这个主要是其队列实现方式决定的,没办法的事。一开始Head节点被设置好了,按照正常逻辑,当当前节点是头节点的时候,此时应该是轮到当前节点所代表的当前线程去tryAcquire,但是在队列实现时,头节点被设置成一个标志位,头节点的下一节点是真实的头节点,而且头节点保存了此节点的waitStatus信息,以此类推,前置节点保存了后置节点的waitStaus信息。尾节点为null,也是个标志位,因此逻辑上的尾节点,不需要保存waitStatus信息。这里不必过度纠结。理解上不会出现问题,当然也可以实现有效节点,但是毕竟不如头节点和尾节点是标志位的实现会更好一些,ConcurrentLinkedQueue的头节点和尾节点也只是标志位
再看release实现:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
这个实现看起来很简单,先是tryRelease,如果成功,唤醒当前节点的下一个节点(如果有的话),注意这个操作是由调用时序决定的, 在进行release之前,必然经历过acquire,而不是头节点的进行acquire将会park,因而要能进行release,当前线程必然位于头节点。如果不满足这个调用时序,按道理将会出错,为了仍保证正确,你需要在tryRelease中对当前状态进行判断。如允许,才返回true.具体的可以看看ReentrantLock的实现
在unparkSuccessor(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.
*/
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;
//这里由于是头节点,其next节点往往不是null,因此正常情况下都是通知头节点的下一个节点
if (s == null || s.waitStatus > 0) {
s = null;
//对于异常情况,头节点的下一节点的waitStatus>0,即为CANCELLED状态,之前解释过,当failed=true的时候才会触发
//cancelAcquire,能够触发的也就是带超时时间tryAcquireNanos()和允许打断tryAcquireInteruptibly()的两种,此时
//触发的是当前节点的取消操作,为什么是从后往前找,需要进一步看cancelAcquire的实现:
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
方法cancelAcquire(Node node),当tryAcquireNanos超时或者tryAcquireInteruptibly被打断的时候被触发:
按照正常的想法,取消当前节点的acquire,只需要
1.如果当前节点为null,直接返回即可
2.不为null,将该节点从队列里面删除:node.next.prev=node.prev, node.next=node.prev=null 即可
3.删除该节点之后,unpark当前节点所对应的线程
但是它并没有直接这么做,为什么呢?
首先,这里会存在并发竞争,试想,多个node当tryAcquireInteruptibly同时被打断,或者同时超时时,而且将当前节点从队列里面删除并不是一个原子操作,而且还需要实现这样的定义:
If a node is cancelled, its successor is (normally) relinked to a non-cancelled predecessor.
其具体实现如下:(有心的同学可以去参考ConcurrentLinkedQueue的remove(Object)操作,观察其异同)
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return;
node.thread = null;
// Skip cancelled predecessors
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// predNext is the apparent node to unsplice. CASes below will
// fail if not, in which case, we lost race vs another cancel
// or signal, so no further action is necessary.
Node predNext = pred.next;
// Can use unconditional write instead of CAS here.
// After this atomic step, other Nodes can skip past us.
// Before, we are free of interference from other threads.
node.waitStatus = Node.CANCELLED;
// If we are the tail, remove ourselves.
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
// If successor needs signal, try to set pred's next-link
// so it will get one. Otherwise wake it up to propagate.
int ws;
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 {
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
经过查看源码主要是第二步有差异,虽然node.prev指向了其前方最近的一个状态不是CANCELLED的节点,在取消过程中其他取消节点也会这么做,最终达成了这样一种状态:
这里没有考虑一致性,可能会出现某个cancelled节点的前置节点又被取消,此时其实不要紧,原因是:
1.cancelled状态是不可逆的,当某个节点被取消,虽然该节点仍在队列里,但是在acquire从头节点往后筛的时候,waitStatus>0的节点将会被忽略,从而被删除出队列,
shouldParkAfterFailedAcquire如下:
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;
}
一连串的CANCELLED节点将会从队列中删除掉,很骚气。
另外此处也存在几个问题,
1.由于之前cancelAcquire的时候,取消节点没有被真正被删除,可能会造成已取消的节点无法被垃圾回收,队列过长的问题,唤醒下一个节点可能会搜索较长时间
2.如果某个当前节点一直park住,而有没有新节点加入,如果其他节点多次触发cancelAcquire,可能会造成CANCELLED节点不断增长,这个问题和1其实差不多
当然了 上述情况只是极端情况,实际操作中并不存在。就算存在,也不会这么极端,在可控范围之内
上述是exclusive模式下的acquire和release
接下来是shared模式的:
public final void acquireShared(int arg)
以共享模式获取对象,忽略中断。通过至少先调用一次 tryAcquireShared(int)
来实现此方法,并在成功时返回。否则在成功之前,一直调用 tryAcquireShared(int)
将线程加入队列,线程可能重复被阻塞或不被阻塞。
参数:
arg
- acquire 参数。此值被传送给 tryAcquireShared(int)
,但它是不间断的,并且可以表示任何内容。
public final void acquireShared(int arg) { if (tryAcquireShared(arg) < 0) doAcquireShared(arg); }
可以看到 也是先调用tryAcquireShared(arg),完成至少先调用一次 tryAcquireShared(int),如不成功,再进行
doAcquireShared(arg); 在该方法中用到了与exclusive类似的逻辑,实现如下:
private void doAcquireShared(int arg) { final Node node = addWaiter(Node.SHARED); boolean failed = true; try { boolean interrupted = false; for (;;) { final Node p = node.predecessor(); if (p == head) { int r = tryAcquireShared(arg); if (r >= 0) { //这里的r就相当于剩余凭证,尝试将凭证尽可能的传播给更多节点 setHeadAndPropagate(node, r); p.next = null; // help GC if (interrupted) selfInterrupt(); failed = false; return; } } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }
整理来看,与之前的acquireQueued类似,也是先addWaiter进队,然后开一个for循环,for循环内部一旦当前节点的前置节点是头节点了,进行tryAcquireShared,成功之后删除头节点,设置当前节点为头节点,如果在这期间发生过Thread.interrupt,则调用selfInterrupt()恢复标志位,如果当前不是头节点,判断shouldParkAfterFailedAcquire,然后进行阻塞等待被唤醒,与之前的逻辑几乎一样,所不同的在于tryAcquireShared成功之后,原先的setHead变成了setHeadAndPropagate,这也是share和exclusive的重要区别之一,exclusive往往是tryAcquire成功之后,后面的继续等,当release的时候,唤醒后继节点,而shared模式则是当前节点tryAcquireShared成功之后,继续检查后续节点也能否接着进行tryAcquireShared,因为是共享锁嘛,当有一个线程能够tryAcquireShared,意味着release了一批“凭证”,后续节点应尽可能的争夺这些凭证,这就产生了“传播过程”,直到凭证全部被争夺完,即tryAcquireShared返回值小于0。当release的时候亦是如此,先是执行tryReleaseShared,如果释放成功,且返回值为true,代表允许唤醒后续节点,也将执行节点唤醒的传播过程。
共享模式实质就是控制一定量的线程并发执行,那么拥有资源的线程在释放掉部分资源时就可以唤醒后继等待结点
而独占模式则是同一时刻仅允许一个线程执行
传播过程实现在doReleaseShared() (我也不知道这个函数为什么起这个名字,因为真正释放“凭证”的是在tryReleaseShared中)
private void doReleaseShared() { /* * Ensure that a release propagates, even if there are other * in-progress acquires/releases. This proceeds in the usual * way of trying to unparkSuccessor of head if it needs * signal. But if it does not, status is set to PROPAGATE to * ensure that upon release, propagation continues. * Additionally, we must loop in case a new node is added * while we are doing this. Also, unlike other uses of * unparkSuccessor, we need to know if CAS to reset status * fails, if so rechecking. */ for (;;) { Node h = head; if (h != null && h != tail) { int ws = h.waitStatus; if (ws == Node.SIGNAL) { if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue; // loop to recheck cases unparkSuccessor(h); } else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; // loop on failed CAS } if (h == head) // loop if head changed break; } }
这段代码的状态流转也很骚气,一开始怎么看也看不明白,我们先理一下程序执行流程,分析可能执行的情况:
为了表示share模式的特点,我们这样做
1.线程A直接tryAcquireShared成功,不入队
2.线程B也直接tryAcquireShared成功,不入队
3.线程C执行tryAcquireShared不成功,入队,执行for循环,由于当前节点的前置节点是头节点,再次tryAcquireShared,由于A,B都没有release,因而再次失败,shouldPark将其waitStatus从0->SIGNAL,下一轮for循环由于AB仍然没有释放(或者释放了,try成功,并修改头节点,并通知后续节点,但这种情况较为简单,不予讨论),再次失败,shouldPark判定为true,进行阻塞等待被唤醒
4.线程D执行tryAcquireShared不成功,入队,执行for循环,由于当前节点的前置节点不是头节点,因此不进行try,shouldPark将其waitStatus从0->SIGNAL,再次进行for循环,进行阻塞等待被唤醒
5.线程A,B的任意一个执行完成,或者同时执行完成,并发调用release,为了情况复杂一点,我们假设同时完成,同时调用release,且try都返回true,同时执行doReleaseShared去同时唤醒后继节点
6.由于AB不在队列里面,当然在不在队列里面无所谓,反正唤醒操作都是从队列头节点开始,开启一个for+cas,
此时头节点的waitStatus是SIGNAL(注意,根据前面分析,当前节点的waitStatus保存在其前置节点上),状态为SIGNAL,cas修改头节点的waitStatus为0,相当于一个新进节点,cas成功者线程唤醒线程C,调用成功者线程检查,线程C继续执行,注意此时只可能唤醒队列中的一个线程,一定要注意理解这句话!!!!! 虽然这样,依然有可能有多个新线程来和刚唤醒的线程C去并发执行acquireShared,导致线程C又失败,再次park住。 cas失败者线程继续for循环,执行compareAndSetWaitStatus(h, 0, Node.PROPAGATE) 然后检查当前节点变化情况,如h!=head 说明头节点被移除,我们之前的cas操作都落在了过时节点上,因而对现有队列无影响,此时按道理来说,我们有两个选择,1.退出循环,等待下一波线程去唤醒 2.继续循环,直到方法调用完成前,将cas操作实实在在的落到当前队列头节点。 显然2更符合通知头节点语义,AQS也是这么做的。
还有一个问题 PROPAGATE状态其含义是什么,怎么处理?
未完待续----------------------