
一、什么是AQSAQS英文全称为AbstractQueueSynchronizer即抽象队列同步器。它的功能和Synchronized类似都是提供多线程同步管理的工具但是AQS的功能更丰富。二、AQS数据结构1.state同步状态state是volatile类型的int变量用于标记当前的锁状态。提供get和set方法读取和设置变量更重要的是提供了基于CAS实现的compareAndSetState方法用于多线程抢占该锁资源。当state为0时表示锁资源可获取state大于0时表示重入次数无重入则为12.node线程节点Node节点Node节点用于保存和表示并发获取该锁资源的线程每个等待线程会被封装成一个Node放入等待队列中。Node类型包含下面字段。thread当前节点所代表的线程。waitStatus节点等待状态主要有以下几个取值SIGNAL (-1)当前节点的后继节点需要被唤醒。CANCELLED (1)当前节点因超时或中断被取消不会再参与竞争。CONDITION (-2)当前节点在条件队列Condition中等待。PROPAGATE (-3)在共享模式下状态需要无条件传播。0初始状态。prev / next指向前驱和后继节点的引用用于构建双向队列。nextWaiter用于在条件队列中指向下一个节点。3.等待队列当并发线程获取锁资源失败时会以Node节点形式进入等待队列。该等待队列类似CLH队列。head队头指针指向队列队头第一个Node节点tail队尾指针指向队列中最后一个节点Node队列由Node对象通过Node.prev和Node.next指针串联起来的双向链表结构compareAndSetHead方法private final boolean compareAndSetHead(Node update) { // headOffset(头指针), null(head原来空的null引用), update(传入的新head引用) return unsafe.compareAndSwapObject(this, headOffset, null, update); }三、AQS工作原理下面通过三个线程A、B和C看看AQS是如何工作的。1.初始状态无竞争AQS 引用head nulltail null。链表不存在任何Node对象。资源状态state 0子类定义 0 为空闲。2.线程 A 首次调用acquire()线程 A 执行 AQS 的acquire(int arg)模板方法。调用tryAcquire(arg)钩子方法子类检查state当前为 0返回true。AQS 反应acquire方法中的if (!tryAcquire(arg) ...)短路直接返回。结果线程 A 直接通过无需创建队列。此时head和tail依然为null。3.线程 B 调用acquire()此时线程 A 持有资源state 1线程 B 开始执行acquire。3.1 试探失败调用tryAcquire(arg)子类看到state 1返回false。3.2 入队addWaiter与队列初始化enqAQS 执行addWaiter(Node.EXCLUSIVE)因为tailnull进入enq方法通过自旋加入队尾private Node enq(final Node node) { // 这里的 node 是线程B的节点 for (;;) { // 线程B在这里自旋 Node t tail; if (t null) { // 第1次循环true // 必须初始化虚拟头节点 if (compareAndSetHead(new Node())) // 线程B 通过CAS设置 head tail head; // 线程B 将 tail 指向刚创建的虚拟头节点 } else { // 第2次循环会进入这里 node.prev t; if (compareAndSetTail(t, node)) { t.next node; return t; } } } }循环第 1 次获取tail发现tail null。AQS 创建第一个节点虚拟头节点compareAndSetHead(new Node())thread nullwaitStatus 0通过 CAS 将head指向新建的head节点然后将tail也指向head完成等待队列初始化。循环第 2 次获取tail现在tail指向head。操作nodeB将线程 B 的nodeB.prev指向 head。操作tail通过 CAS 将tail从head改为指向nodeB。操作head将head.next指向nodeB。为什么要自旋第二次才设置head不能初始化虚拟头节点后直接设置head吗不能因为可能存在多个线程同时初始化头节点。3.3 开始自旋等待acquireQueued与阻塞final boolean acquireQueued(final Node node, int arg) { for (;;) { final Node p node.predecessor(); if (p head tryAcquire(arg)) { // ① 只有满足位置条件才抢锁 setHead(node); return false; } if (shouldParkAfterFailedAcquire(p, node) // ② 无论是否抢锁都要执行 parkAndCheckInterrupt()) { return true; } } }完成队列初始化和入队后线程 B 进入acquireQueued自旋(nodeB, arg)由于B是第一个等待者所以可以再次尝试获取锁如果还是不成功那么就将前驱节点p对于B来说是head置为SIGNAL(-1)在前驱节点释放资源时会唤醒当前节点。获取前驱p nodeB.prev指向虚拟头节点。p head成立B 是第一个真实等待者。由于B是第一个等待者所以可以再次调用tryAcquire(arg)state仍为 1失败。调用shouldParkAfterFailedAcquire(p, nodeB)检查前置节点head的waitStatu情况1waitStatus为 0当前场景下是这种情况通过 CAS 将虚拟头节点的waitStatus改为SIGNAL(-1)。语义“虚拟头节点你释放资源时必须唤醒我B然后返回 false。为什么不在enq的时候设置status因为如果存在多个线程初始化头节点那么入队后并非都是第一个等待者同时自旋的时候还可以重新获取一次锁这样尽可能避免队列等待。情况2waitStatus为 大于0CANCELLED1说明前置节点已经超时或中断此时需要循环将当前节点越过所有死掉的节点将nodeB挂到最近的一个活着的节点后面修改node.prev和活节点.next这是为什么要传nodeB的原因。然后返回false。情况3waitstatus为1说明已经就绪什么都不需要做返回true。若shouldParkAfterFailedAcquire返回true如情况3则停止自旋调用parkAndCheckInterrupt()通过LockSupport.park(this)挂起线程 B。若shouldParkAfterFailedAcquire返回false则继续自旋。如果是情况1返回的true则再次自旋时会变成情况3返回true停止自旋如果是情况2返回的false则再次自旋可能进入情况1或者情况3情况1又会进入情况3如果该线程无法在过程中获取到锁那么最终都会返回true停止自旋。为什么不一步到位而要自旋分多个步骤完成剩下的事情实际最基本的思想就是每次操作时你获取到的当前AQS状态最好都是最新的你在检查一次状态后做了很多事情这些事情在做的时候整个AQS随时会发生变化再次自旋获取状态只是一次读操作一次读操作避免掉很多无效的操作这绝对是有意义的同时在自旋过程中每次自旋都会重新尝试获取锁如果并发数量不高则不会挂起线程尽最大可能避免了挂起排队造成的上下文切换下面针对情况1和情况2返回false然后自旋的原因详细说说情况1假设情况1检查到前置状态为0将前置节点设置为1后就将当前线程挂起那么可能会出现一种情况在你尝试挂起之前前置节点已经设置为1然后前置节点可能会马上失效去唤醒当前节点假设这个唤醒动作比挂起动作执行得快最终当前节点就会先接收到唤醒命令后接收到挂起命令前置节点的唤醒命令已经发出并且前置节点已经失效从而当前节点永远没有机会被唤醒。同时在自旋时会重新尝试获取锁如果能获取成功则不会挂起线程避免了线程上下文切换带来的开销。情况2再假设情况2完成后挂到了最后面的一个有效节点的后面然后直接修改前置状态那么可能会出现当前节点已经成了第一个等待节点很有可能马上就可以执行而直接设置前置状态为1后第二次自旋会直接获取到该线程那么这次设置前置状态的CAS就是浪费了的。而这里不设置前置状态为1也没影响因为如果它成了第一个等待节点那么假设锁被释放头节点没有唤醒我们这个第一个等待节点由于释放后锁state已经释放我们自己也会再自旋获取到锁但是这里我们避免了一次CAS写操作带来的性能开销而下次自旋检查当前节点是否是第一个头节点操作是读操作性能消耗远小于CAS写如果自旋中发现我们不是第一个等待节点或者锁没有被释放那么在自旋中再设置前置状态为1就好了虽然多了一次自旋但是这个自旋带来的额外消耗只是检查节点是否第一个等待节点以及检查锁状态尝试获取锁都是读操作对比浪费一个CAS来说还是划算的因为如果能直接获取到的话就可能节省一个CAS操作。如果这次自旋设置了状态下次自旋又会尝试获取锁又多了一次获取锁的机会尽可能避免了线程挂起带来性能消耗。4.线程A释放锁假设上面的线程B操作时是情况1即线程A没有在B进入等待队列的过程中释放。然后线程A释放锁执行release()调用tryReleaseAQS的state变为0返回true。获取当前head指向虚拟节点发现head.ws SIGNAL(-1)。执行unparkSuccessor(head)CAS将head.ws从SIGNAL改为0表示唤醒指令已发出然后不再承担唤醒责任A会将head置为A节点然后由A唤醒BC则由B唤醒。取head.next得到nodeB确认其未取消。调用LockSupport.unpark(nodeB.thread)线程B被唤醒。5.线程B被唤醒线程B从park()返回回到acquireQueued的自旋中重新开始自旋被唤醒后继续获取前驱p nodeB.prev虚拟节点。p head成立。这里要注意一点线程A释放时并没有操作等待队列线程A不是head线程A是获取到锁直接跑起来的A释放只需要修改state锁状态。调用tryAcquire此时state0成功获取资源。执行setHead(nodeB)使当前节点成为新的head哨兵AQS的head引用改为指向nodeB。nodeB.thread nullB退化为新的哨兵。nodeB.prev null断开与旧虚拟头的链接。旧head虚拟节点在B唤醒时失去head指向它的引用根据GC算法没有引用指向的对象会被回收。后续线程B释放唤醒线程C的过程和线程A释放唤醒B是一样的head指向第一个等待线程AQS指向head哨兵的节点始终稳定。四、总结通过上面的讲解应该可以了解到AQS可以很好地管理锁资源抢占那么它对比synchronized一定更好吗不一定synchronized有偏向锁、轻量级锁和重量级锁三个等级synchronized的性能在无竞争的情况下性能是最好的只需要对比mark wordAQS的话则是检查state性能也很快但是AQS的性能还是略低于synchronized。而synchronized的轻量级锁和AQS的自旋获取线程性能差不多轻量级锁也是自旋CAS实现的。synchronized升级到重量级锁之后其性能比AQS的非公平锁实现要差且synchronized不会随着竞争降低降级回到轻量级锁或偏向锁所以在竞争比较强的情况下使用AQS非公平锁性能更好但是如果必须避免线程饥饿可能某些等待线程一直无法获得资源时只能使用AQS公平锁synchronized是非公平锁。下面是不同锁实现的差异对比。锁类型无竞争场景单线程/空闲轻量竞争线程交替极少冲突中等竞争偶发阻塞锁持有时长不定高竞争大量线程频繁阻塞最适应场景核心实现原理1.synchronized偏向锁⭐⭐⭐⭐⭐绝对王者耗时~1-3 ns仅比对线程ID零CAS、零内存屏障。⚠️性能断崖触发偏向撤销需STW耗时飙升退出此赛道升级。—已升级—已升级绝对单线程环境如单线程循环、无竞争的Vector操作对象头Mark Word存储线程ID无任何同步指令。2.synchronized轻量级锁⭐⭐⭐⭐很快耗时~10-20 ns单次CAS替换Mark Word。⭐⭐⭐⭐并列第一耗时~50-100 ns用户态自旋CAS消化冲突无内核切换。⚠️拐点出现自旋超阈值后膨胀为重量级锁性能开始下滑。—已永久升级为重量级不可降级线程交替执行锁持有时间极短几乎无并发冲突CAS 将对象头复制到栈帧锁记录Displaced Mark Word。3. AQS 非公平锁⭐⭐⭐⭐略逊于轻量级耗时~20-50 nsCAS修改state多了一层CLH队列检查。⭐⭐⭐⭐与轻量级持平耗时~50-100 nsCAS重试不会膨胀升级状态稳定。⭐⭐⭐⭐⭐开始反超耗时~1-5 μs少量线程入队阻塞但利用唤醒延迟让新线程插队吞吐量最高。⭐⭐⭐⭐高吞吐冠军耗时~10-50 μsCLH队列高效插队机制将切换延迟“抵消”为有效工作。中高并发核心链路追求极致吞吐量允许短时插队CAS操作stateCLH双向队列释放时仅唤醒head.next不阻止新节点CAS抢锁。4.synchronized重量级锁⭐遗留陷阱因不可降级若历史有过竞争无竞争时仍走Mutex耗时~0.5-1 μs。⭐⭐表现不佳因不可降级轻竞争下依然走内核互斥量耗时~1-5 μs。⭐⭐逊于AQS非公平耗时~5-20 μs虽有自适应自旋但锁移交由内核调度缺乏用户态优化。⭐⭐⭐中规中矩耗时~20-100 μs由操作系统调度虽稳定但吞吐量低于AQS非公平。竞争不频繁且不在意降级损耗或传统遗留代码JVM维护ObjectMonitor底层基于pthread_mutex_t或futex完全由内核调度。5. AQS 公平锁⭐⭐自带拖累每次加锁检查前驱节点耗时~50-100 ns比非公平慢。⭐过早阻塞强制新线程入队并park()放弃CAS抢锁耗时~5-20 μs。⭐吞吐量垫底耗时~20-100 μs频繁维护CLH节点状态SIGNALCAS导致CPU缓存行失效频繁。⭐高并发最差耗时~50-200 μs上下文切换最频繁Java层CAS开销比内核futex还重。严格防止线程饥饿低并发下严格FIFO顺序继承AQStryAcquire中强制检查CLH队列是否有等待者有则直接入队严格FIFO唤醒移交。6. 单线程池(newSingleThreadExecutor)⭐⭐⭐业务零锁耗时~100-300 ns队列内存屏障。注意虽无锁竞争但跨线程传递需刷新CPU缓存比偏向锁慢。⭐⭐⭐⭐稳定输出耗时~1-5 μs无业务锁争用队列采用CAS或轻量锁绝不膨胀。性能优于重量级锁。⭐⭐吞吐量瓶颈耗时~20-100 μs单核处理能力上限成为瓶颈队列开始积压内存压力增大。⭐严重积压/阻塞耗时~100 μs-∞单线程处理不过来队列爆满导致提交线程阻塞有界队列或OOM无界队列吞吐量远低于AQS非公平。严格的状态隔离如日志异步刷盘、单线程GUI事件派发且业务QPS必须小于单核处理能力依靠BlockingQueue如LinkedBlockingQueue实现生产者-消费者模型。完全消除共享状态通过队列的happens-before内存语义传递数据。JAVA提供了基于AQS实现的多种实现类欲知后事如何请关注我。