JDK源码学习05-简单理解AQS和FairSync源码
1. 简介
1.1 说明
ReentrantLock的实现依托于大名鼎鼎AQS实现,AQS的注释上说了,建议使用内部工具类的方式实现AQS抽象类,作为同步工具。所以ReentrantLock内部主要有NonfairSync(非公平锁)和FairSync(公平锁)两种实现方式。
AQS就是AbstractQueuedSynchronizer即抽象队列同步器,使用模版方法设计模式,由AQS提供同步和核心算法,而把核心代码中调用的关键函数留给继承的子类来实现,这就是模版方法设计模式。DougLea建议使用实现AQS的内部类作为工具类来实现自定义的同步类。
1.2 透过FairSync理解AQS
我们可以看到ReentrantLock只不过是实现了Lock接口罢了,所以ReentrantLock的锁功能实现必然不因为Lock接口,那是怎么实现的呢?
public class ReentrantLock implements Lock, java.io.Serializable
1.3 ReentrantLock的内部类
ReentrantLock内部主要有NonfairSync(非公平锁)和FairSync(公平锁)两种实现方式。
我们可以看见ReentrantLock内部主要有三个内部类。
# 由于公平锁和非公平锁有一些相似的功能,可以抽象起来
abstract static class Sync extends AbstractQueuedSynchronizer {
# 非公平锁
static final class NonfairSync extends Sync {
# 公平锁
static final class FairSync extends Sync {
2. 顺着逻辑理清FairSync脉络
2.1 根据构造函数选择公平或是非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
2.2 上锁
接下来的流程非常重要,建议仔细阅读每一步的注释
- 首先我们知道lock,初始时如果获取到锁,就不会阻塞;如果初始时没有获取到锁,就会阻塞
- 表面上,上面一句话是废话,但是在追读源码的时候很容易忽视
- 清楚上面之后,下面的acquire函数的功能就非常容易解释了,获取到锁就放行,把它当作一个空函数;没获取到锁就阻塞,一直阻塞到获取到锁,然后放行。(这就是锁的究极含义)
public void lock() {
sync.lock();
}
// FairSync中的函数
final void lock() {
acquire(1);// AQS函数
}
// AQS中的函数 接下来就是重点了 我们仔细追踪每一步流程
// -
public final void acquire(int arg) {
// 4. 先使用tryAcquire尝试获取锁,如果获取到,if条件整个为false,就可以直接放行了,就很棒。
// 5. 如果tryAcquire没有获取到锁,就会执行后面的条件了,首先执行的是addWaiter函数,addWaiter函数
// 的参数只是用来标记状态的,不需要关心。现在先执行addWaiter函数。
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
- 现在我们细看上面的步骤,先使用tryAcquire尝试获取锁,如果获取到,if条件整个为false,就可以直接放行了,就很棒。tryAcquire是AQS提供给我们实现的方法,用来实现自己的锁逻辑,我们先简略看看即可,不必深究。下面代码的逻辑非常复杂,特别是hasQueuedPredecessors中有很多种情况,我们留着后面再解析(这不过是实现公平锁特有的代码段,而我们更希望研究调用这个tryAcquire函数的框架AQS----模版方法设计模式)
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// hasQueuedPredecessors的英文含义是已经给队列排队前置节点
// 在队列不是空的情况下 如果当前线程是第一个排队的节点(即链表第二个节点) 函数返回true否则false
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;
}
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
- 如果tryAcquire没有获取到锁,就会执行后面的条件了,首先执行的是addWaiter函数,addWaiter函数的参数只是用来标记状态的,不需要关心。现在先执行addWaiter函数,addWaiter的意思就是把当前线程添加到同步队列的尾部。
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不为null 先尝试在本函数入队 失败就使用enq循环入队 鬼知道为什么要这多余的一步
if (pred != null) {
// 进队操作 参考enq函数的else
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 函数就在下方
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
// 只有在第一次进队才出现t为null的情况
// 初始队头--》象征获取锁的线程 但不是 队头node中thread永远为null
// 初始化第一次的头节点之后 就可以真正进队当前节点了(第二次for循环)
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 一直到进队成功 才结束函数
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
- 上面的代码看起来很长,其实也就是 死循环+CAS 将当前线程加入队尾罢了。
# 唯一需要注意的是,AQS中的头节点是懒加载的形式,阻塞的节点只能从第二个节点开始排队。
- 此时我们再次回到第三步,及下述代码,此时我们可以将条件简化了。我们假设有人先获取了锁,然后我才来,此时tryAcquire返回false,所以现在我们来研究以下acquireQueued函数
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
if (acquireQueued(new Node(“我是进来排队的线程”), arg))
selfInterrupt();
// 死循环!! 此处的node是上一部中被添加到AQS尾部的节点
// 此时是tryAcquire失败后 第一次尝试再次尝试获取锁(由于是公平锁 则必须是第一个排队的人)
// 如果获取失败,就会进入下一个判断
// shouldParkAfterFailedAcquire我们简要说明一下
//虽然还包含了取消节点的功能 但这里不扩展了 只是为了方便理解的说明
// 只有在前一个节点状态是-1的时候 才会返回true 如果前一个节点状态不为-1 则设置为-1
// 第一次节点状态默认是0 shouldParkAfterFailedAcquire检查之后设为-1
// 故循环一般会被循环一次 两次 在第两次的时候 才会调用parkAndCheckInterrupt方法 进入阻塞
// 如果我这个线程被唤醒 很明显 通过下面逻辑 本函数返回的一定是false
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
// 死循环!! 此处的node是上一部中被添加到AQS尾部的节点
// 此时是tryAcquire失败后 第一次尝试再次尝试获取锁(由于是公平锁 则必须是第一个排队的人)
// 如果获取失败,就会进入下一个判断
// shouldParkAfterFailedAcquire我们简要说明一下
//虽然还包含了取消节点的功能 但这里不扩展了 只是为了方便理解的说明
// 只有在前一个节点状态是-1的时候 才会返回true 如果前一个节点状态不为-1 则设置为-1
// 第一次节点状态默认是0 shouldParkAfterFailedAcquire检查之后设为-1
// 故循环一般会被循环一次 两次 在第两次的时候 才会调用parkAndCheckInterrupt方法 进入阻塞
// 如果我这个线程被唤醒 很明显 通过下面逻辑 本函数返回的一定是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 {
if (failed)
cancelAcquire(node);
}
}
// 这个函数不是实际源码 而是我删减后的
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
return true;
else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
- 现在我们基本分析完成了整个AQS的流程