失眠必看的源码分析/高阶面试:AQS核心机制过程原理
FBI WARNING:源码分析,未成熟的Java程序员请在成熟程序员陪同下观看
关于AQS的基础面试话术可以参考面试鸭,或者我之前写过的一个基础版:https://www.codefather.cn/post/1952824912558391297
下面是高阶的AQS源码分析。
参考文献:《深入理解Java高并发编程》作者黄俊。
目的是没有任何目的,纯粹是为了折磨自己,不建议观看,马上退出(^_^)。
AQS核心之acquire过程原理(独占模式)
AQS入队设计
首先通过tryAcquire方法去抢锁,这里AQS没有自己实现,这是个抽象方法,留给并发工具类自己实现。其实就是CAS修改state的逻辑。
▼java复制代码// java.util.concurrent.locks.AbstractQueuedSynchronizer#acquire public final void acquire(int arg) { if (!tryAcquire(arg) && // 1. 尝试直接获取锁,由子类实现 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 2. 失败则入队 selfInterrupt(); } // tryAcquire 由子类实现,例如ReentrantLock中的FairSync protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); }
当tryAcquire执行失败时,会执行acquireQueued方法,表示要将节点加入队列。
核心逻辑是addWaiter方法。
此时的巧妙之处在于,AQS会先直接执行CAS替换尾指针,如果此时队列节点已经被初始化了,以及没有其它线程竞争的话,那就美滋滋,直接返回节点,入队成功。
▼java复制代码// java.util.concurrent.locks.AbstractQueuedSynchronizer#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; if (pred != null) { // 队列已初始化 node.prev = pred; if (compareAndSetTail(pred, node)) { // 快速CAS入队(只尝试一次!) pred.next = node; return node; // 成功,直接返回 } } enq(node); // 快速路径失败,进入enq方法 return node; }
否则执行enq方法。
以一个for死循环,初始化头节点和尾节点(如果是第一次),再不断CAS自旋,直到节点入队成功。
巧妙之处:没有直接执行死循环自旋CAS,而是先执行一次CAS尝试将节点快速入队,只执行一次,效率高!!避免了for死循环里的各种前置判断。
▼java复制代码// java.util.concurrent.locks.AbstractQueuedSynchronizer#enq 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入队 t.next = node; return t; } } } }
为什么AQS可以先快速执行一次CAS/为什么不直接用for死循环来操作入队?
因为这里是将节点加入到队列里面,前面已经判断了tryAcquire失败了,意味着大概率这个队列已经被初始化了,此时如果线程竞争不激烈的话,只执行一次CAS就能够成功入队(_)
enq方法里会执行不断的CAS自旋,并且会判断是否需要对队列进行初始化,耗资源的操作就在这步,哪怕不需要初始化你也要多几个if判断。所以我们能不走enq方法就不走。
关键条件:初始化完成 && 竞争不激烈 === 一次CAS快速入队成功,效率高!!!
获取队列
上面的addWaiter方法已经将等待线程封装了节点,并加入的队列。
此时需要尝试将当前节点设置为头节点,到了acquireQueued方法。
这个方法主要操作当前节点的前面一个节点。
判断如果当前节点前一个节点为头节点,就轮到当前节点抢锁了。
如果当前节点不是头节点,就得判断当前节点是否允许阻塞,用shouldParkAfterFailedAcquire方法实现,返回true后就执行线程中断操作。
最后如果执行失败了,就调用cancelAcquire,取消锁流程。
整个过程是个for死循环。无限循环上述操作。
▼java复制代码// java.util.concurrent.locks.AbstractQueuedSynchronizer#acquireQueued 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 { if (failed) cancelAcquire(node); // 最终失败(如异常),取消节点 } }
怎么判断是否应该在抢锁失败后中断线程(shouldParkAfterFailedAcquire方法的实现原理)?
AQS维护了4个状态变量:
CANCELLED = 1等待队列中线程被取消的状态SIGNAL = -1等待队列中当前节点后面的线程需要被唤醒CONDITION = -2线程在等待一个条件PROPAGATE = -3唤醒后面节点的行为需要传播,用来多线程同步的0为初始化或正在运行
可以分类为两个部分,当前节点状态 <= 0 的是等待状态,> 0 的是被取消状态(放弃抢锁,等待被GC)
▼java复制代码// java.util.concurrent.locks.AbstractQueuedSynchronizer.Node static final class Node { /** 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; volatile int waitStatus; // ... }
这个方法的实现原理就是:判断当前节点的前面一个节点的状态是否为SIGNAL,如果是,直接返回true,在acquireQueued方法里直接可以中断线程。否则就执行一次CAS将前面节点的状态设置为SIGNAL,为了让当前节点可以放心阻塞。
▼java复制代码// java.util.concurrent.locks.AbstractQueuedSynchronizer#shouldParkAfterFailedAcquire private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; // 获取前驱节点的状态 if (ws == Node.SIGNAL) /* * 前驱节点已经是SIGNAL状态,意味着它释放锁后会唤醒我。 * 我可以安心阻塞了。 */ return true; if (ws > 0) { // 前驱节点被取消了 (CANCELLED > 0) /* * 跳过所有被取消的前驱节点,直到找到一个未被取消的。 */ do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { /* * 前驱节点状态为0或PROPAGATE。 * 通过CAS将其状态设置为SIGNAL,告诉它“释放锁时记得唤醒我”。 */ compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; // 这次先不阻塞,返回上层循环再试一次 }
将前一个节点判断或者设置为SIGNAL状态,就是为了告诉当前节点node可以放心阻塞,因为前一个节点会在释放完锁后唤醒当前节点。这是一个契约,相当于一个承诺。在后面的unparkSuccessor方法里会根据这个状态唤醒后一个节点。
为什么这么做?
避免出现饥饿状态。
这是AQS实现等待-唤醒机制的原理。
即当前节点抢锁失败后,需要告诉前驱节点:释放锁的时候记得唤醒我。
为了让前驱节点知道释放锁后要唤醒后一个节点,AQS就必须不断地将当前节点的前驱节点CAS修改成SIGNAL状态。
上面提到的只是独占模式,即只有一个线程能抢到锁。
还有多个线程抢锁的部分...
AQS核心之acquireShared过程原理(共享模式)
未完待续/(ㄒoㄒ)/~~
