ReentrantLock&FairSync源码分析-AQS排他锁并发控制类实现分析
AQS是concurrent包中同步类的核心,AQS的acquire&release和acquireShared&releaseShared用模板模式定义了并发逻辑的实现,并发控制类通过继承AQS,实现tryAcquire&tryRelease和tryAcquireShared&tryReleaseShared,就能构建出新的并发控制逻辑(ReentrantLock,CountDownLatch等),或使用基于AQS实现的并发控制类构建出功能更复杂的并发控制类(CyclicBarrier,ArrayBlockingQueue等).
AQS有两种锁模式:排它锁和共享锁。排他锁性质的并发控制类需要实现AQS的tryAcquire和tryRelease方法,共享锁性质的并发控制类需要实现tryAcquireShared和tryReleaseShared方法。下面基于ReentrantLock&FairSync的源码分析,说明如何利用AQS排它锁实现并发控制类。
分析过程先说明如何基于AQS实现ReentrantLock,最后解析整个过程中的细节。

ReentrantLock.lock(): void {
sync.lock(); //sync是FaiySync实例,最终调用AQS.acquire(1)
}
ReentrantLock.unlock(): void {
sync.release(1); //sync是FaiySync实例,最终调用AQS.release(1)
}
lock和unlock最终调用AQS的acquire和release方法,其代码如下:
AbstractQueuedSynchronizer.acquire(int arg): void {
//1、tryAcquire实现加锁,返回是否成功,由子类实现;
//2、tryAcquire失败,执行addWaiter:new Node(Thread.currentThread, EXCLUSIVE),加入AQS的waiter
// 链(AQS waiter链特点:head对应的是当前正在执行的线程,但不持有有效数据,head.next是unlock默认唤醒的线程的节点,具体实现后文分析);
//3、执行acquireQueued:当前线程暂停进入waiting状态,被唤醒并获得cpu时间分片后重新尝试获取锁;
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
//置当前线程的interrupt状态
selfInterrupt();
}
AbstractQueuedSynchronizer.release(int arg): boolean {
//tryRelease逻辑由子类实现,返回释放锁是否成功:false不代表失败,在重复加锁的情况下,false表示还未释放所有的锁;
if (tryRelease(arg)) {
//head为AQS等待链的头
Node h = head;
//waitStatus==0:0默认值,head的waitStatus为0表示AQS的waiter链只有head节点(如果有后续节点,一定会将head的waitStatus置为其他状态,具体逻辑稍后分析)
if (h != null && h.waitStatus != 0)
//唤醒AQS waiter链中的某个线程;
unparkSuccessor(h);
return true;
}
return false;
}
从上面的代码可以发现AQS acquire和release是模板模式的实现,具体的子类实现只需要编写加锁(tryAcquire)和解锁(tryRelease)逻辑,而不用操心加锁失败后线程如何暂停最终又如何被唤醒,和解锁成功后如何唤醒等待线程的实现逻辑,极大简化了并发控制类的实现难度。下面是FairSync具体的tryAcquire和tryRelease的代码实现分析:
FairSync.tryAcquire(int acquires): boolean {
final Thread current = Thread.currentThread();
int c = getState();
//state出事化值为0,被别的线程加锁后会大于0,故c == 0,表明没有线程加锁
if (c == 0) {
//1、!hasQueuedPredecessors:公平判断。如果AQSwaiter链为空,或waiter链第一个待唤醒Node的thread是当前线程,则结果为true。此判断实现公平锁的逻辑。去掉即
// 是NonFairSync的tryAcquire实现逻辑;
//2、cas设置state(此操作执行的时候是没有加锁的,故需要cas执行),如果成功,表明当前线程加锁成功;如果失败,表明在getState操作到cas操作
// 期间内,有别的线程加锁成功;
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
//设置持有排它锁的线程,判断中的cas操作成功,加锁已经成功,故set无需cas操作
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;
}
FairSync.Sync.tryRelease(int releases): boolean {
//解锁的过程中,线程是持有锁的,故无需cas操作
//加锁次数递减
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//加锁次数为0,表明当前线程已经完全释放了锁
if (c == 0) {
//返回解锁完成,AQS根据此返回值唤醒锁上的等待线程
free = true;
setExclusiveOwnerThread(null);
}
//更新state
setState(c);
return free;
}
从分析中可以看到,加锁解锁的实现是通过操作AQS的state变量来实现的(基于AQS实现的并发控制类实现逻辑都是通过操作state来实现相关并发语义的)。
到此,我们基本分析完了ReentrantLock的实现,可以看到,如果要基于AQS排它锁实现一个并发控制类,需要:
1、根据自己的控制逻辑实现tryAcquire和tryRelease;
2、tryAcquire和tryRelease围绕state变量实现;
3、在tryAcquire实现内,加锁成功前的更新操作要调用AQS提供的cas完成;
4、tryAcquire&tryRelease内不能有异常发生,否则会导致一些不可预测的情况发生;可试想ReentrantLock.unlock的tryRelease发生了异常会导致什么情况;
以上分析仅仅告诉我们怎么基于AQS实现自己的并发控制类,未涉及整个加锁流程中加锁失败后的逻辑,与解锁成功后如何唤醒线程的逻辑。如果有兴趣,可以继续看下面的分析。
前面的分析提到AQS.accquire中,加锁失败后会执行addWaiter和acquireQueued操作,addWaiter会将当前线程加入到一个待唤醒线程的waiter链,acquireQueued会暂停线程的执行并等待被唤醒后重新执行tryAcquire,下面是代码分析:
//将当前线程加入waiter链
AbstractQueuedSynchronizer.addWaiter(Node mode): Node {
//mode = Node.EXCLUSIVE,排他模式
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
//如果当前waiter链已经有元素
if (pred != null) {
node.prev = pred;
//cas设置当前线程的节点为tail
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//在循环中cas设置tail
enq(node);
return node;
}
//在循环中使用cas操作将当前thread加到waiter链的队尾
AbstractQueuedSynchronizer.enq(Node node): void {
for (;;) {
Node t = tail;
//如果waiter链为空,则初始化 head = tail = new Node()
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
//否则cas设置tail = node
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
}
执行完addWaiter后,一定会将当前线程加入到waiter链,比如thread1和thread2调用同一个ReentrantLock实例的lock&unlock,如果thread1调用ReentrantLock.lock成功,那么在thread1未unlock前,thread2调用lock并执行完addWaiter后构建了如下的双向waiter链:head = Node(null,null, SIGNAL)->Node(thread2, EXCLUSIVE,null) = tail。
接着分析如何暂停线程,和线程如何在被唤醒后重新获取锁,具体源码如下:
AbstractQueuedSynchronizer.acquireQueued(final Node node, int arg): boolean {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
//node.prev,null时抛出空指针异常。为什么:null时表示node是头结点,头结点可以看作
//当前运行的线程对应的Node,不需要后续操作
final Node p = node.predecessor();
//如果当前线程对应的Node是head后的第一个Node,并且加锁成功,当前线程执行进入临界区
if (p == head && tryAcquire(arg)) {
//将node设置为头结点(此处代码可以看到,head节点即是当前线程对应的Node,只不过head节点的thread这些信息会清除)
setHead(node);
p.next = null; // help GC
failed = false;
//返回线程中断标志
return interrupted;
}
//需要暂停线程;暂停线程并保存中断信号
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
} finally {
//for循环内抛出了异常
if (failed)
//更新waiter链元素,如果有必要,唤醒waiter链中的某个Node的线程
cancelAcquire(node);
}
}
}
AbstractQueuedSynchronizer.shouldParkAfterFailedAcquire(Node pred, Node node): boolean {
int ws = pred.waitStatus;
//如果node前面节点的waitStatus为SIGNAL,那当前节点对应的线程肯定需要暂停:因为AQS是按waiter
//链的先后顺序唤醒线程的。反过来说,如果一个线程暂停了,那它对应Node.prev.waitStatus是SIGNAL(除非另一个线程修改了)
if (ws == Node.SIGNAL)
return true;
//如果node前面节点的waitStatus为CANCEL(>0的值只有CANCEL)
if (ws > 0) {
//以node为起始,向前删除waiter链中CANCELLED状态的节点
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//将node前面节点的waitStatus置为Node.SIGNAL:故acquireQueued的下一次循环调用此方法,会返回true,最终暂停线程
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
AbstractQueuedSynchronizer.parkAndCheckInterrupt(): boolean {
//暂停线程。linux机java线程是映射到内核线程的,线程的调度最终由内核完成
LockSupport.park(this);
return Thread.interrupted();
}
从上面的分析中,大致归纳出一下tryAcquire失败后的代码逻辑:
1、将当前线程加入AQS waiter链;
2、进入循环:正常/异常
2.1、当前线程对应的Node是head.next,并且加锁成功,则更新waiter链的head为当前线程对应的node,返回继续执行后续的业务代码。此处tryAcquire两种情况可以执行成功:在当 前线程第一次tryAcquire失败到本次tryAcquire期间,持有锁的线程释放了锁;持有锁的线程释放了锁,被唤醒线程成功加锁。
2.2、2.1的判断失败,判断当前线程是否需要暂停(node.prev.waitStatus == SIGNAL)(实现逻辑在for循环内,正常情况下for循环的第二次一定会返回true)。如果需要则暂停线程 的执行;不需要,重复2.1;
2.3、waiter链中的某个Node被持有锁的线程唤醒,重复2.1;
思考一个问题:在acquire方法内,tryAcquire失败后,为什么不立即暂停线程,反而在最终暂停前,当前线程至少还有一次机会再次tryAcquire?
前面看到,acquireQueued的for循环外部有finally,finally用于保证for循环内部发生异常的情况下,unpark信号量不会丢失。代码分析如下:
AbstractQueuedSynchronizer.cancelAcquire(Node node): void {
if (node == null)
return;
node.thread = null;
Node pred = node.prev;
//以node为起始,往前删除waiter链中CANCELLED状态的节点
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
Node predNext = pred.next;
//发生异常的线程对应的节点waitStatus置为CANCELLED
node.waitStatus = Node.CANCELLED;
//如果当前节点是尾节点,更新tail和head.next
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
int ws;
//如果异常节点不是head.next,并且异常节点前面有等待唤醒的节点(waitStatus判断),并且异常节点前面节点是有效的(thread判断);
if (pred != head && ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL)))
&& pred.thread != null) {
//删除node
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
//否则以异常节点为开始,唤醒等待链中的某个节点
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
acquireQueued执行后,得到如下结果:
1、正常情况下
1.1 当前线程在本次cpu时间分片内获取到了锁tryAcquire成功;
1.2 被暂停(并等待被唤醒重新获取锁);
2、异常情况下,发生异常的Node从waiter链中删除;如果有必要,以node为起点查找并唤醒某个线程;
再看释放锁的代码: AQS.release调用参数值为head
AbstractQueuedSynchronizer.unparkSuccessor(Node node): void {
int ws = node.waitStatus;
if (ws < 0)
//注意此操作,对cancelAcquire影响
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
//node是tail(tail.next == null)或第一个待唤醒的node.waitStatus为CANCELED
if (s == null || s.waitStatus > 0) {
s = null;
//从tail往前查找,知道找到waiter链最前面waitStatus<=0的非head Node
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
//最终由内核唤醒线程
LockSupport.unpark(s.thread);
}
释放锁的逻辑相对简单,从waiter链的head开始查找到第一个waitStatus <= 0的Node(从tail查找的结果是一样的);如果有,唤醒这个Node的thread;并且head.waitStatus=0;
前面提到acquire操作中tryAcquire失败后线程暂停前至少会再执行一次tryAcquire,个人是这么认为的:基于jvm统计数据发现实际应用中锁冲突的情况是比较少的(如果你的应用有很多,那你应该考虑优化),即使发生了锁冲突,大部分情况下锁是能被快速释放的,因此编码者认为在有限时间内自旋(浪费cpu)付出的代价是小于直接暂停线程导致线程上下文切换的代价的。自旋锁的方式认为通过有限消耗cpu带来的性能提升会大于线程调度带来的性能替身,自旋在jvm里有许多的应用,如偏向锁膨胀为轻量级锁直至重量级锁的过程中,也采用了自旋。
最后,给出一张ReentrantLock.lock大致的流程图:


浙公网安备 33010602011771号