ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

深入解析Java AQS核心原理

深入解析Java AQS核心原理 Java AQS原理简介前言Java AQS原理解析AQS 核心设计思想与系统级架构核心数据结构Node 节点源码剖析独占锁模式获取与释放源码深剖1. 独占锁获取acquire 流程剖析快速入队与兜底入队addWaiter 源码队列内自旋与阻塞挂起acquireQueued 源码挂起条件判定shouldParkAfterFailedAcquire 源码线程系统级挂起parkAndCheckInterrupt 源码2. 独占锁释放release 流程剖析唤醒后继节点unparkSuccessor 源码共享锁模式获取、传播与释放源码深剖1. 共享锁获取与传播acquireShared setHeadAndPropagate传播链的关键setHeadAndPropagate 源码2. 共享锁释放的核心doReleaseShared 源码AQS 底层硬件级与 OS 级支持1. CAS 与 Memory Layout内存布局2. LockSupport.park/unpark 的系统调用总结前言本文旨在记录近期研读Java源码的学习心得与疑难问题。由于个人理解水平有限文中内容难免存在疏漏恳请读者不吝指正。Java AQS原理解析AQS 核心设计思想与系统级架构AbstractQueuedSynchronizer简称 AQS是 Java 并发包java.util.concurrent(JUC) 的基石。ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock 等核心同步组件皆依赖 AQS 构建。从系统架构设计角度来看AQS 是一个基于模板方法模式的、自旋与阻塞结合的、先进先出FIFO的双向队列同步框架。其核心设计由以下三大支柱构成状态管理State使用一个volatile修饰的 32 位整型变量表示同步状态通过基于 CPU 原语的 CASCompare-And-Swap操作保证状态修改的原子性。CLH 变体队列FIFO Queue一个双向链表结构的同步等待队列。当线程获取同步状态失败时AQS 会将当前线程及其等待状态封装成一个Node节点并将其加入队列尾部同时阻塞当前线程。线程调度Park/Unpark利用LockSupport.park()和LockSupport.unpark()封锁和唤醒线程在底层对应操作系统的线程挂起与唤醒如 Linux 环境下的pthread_mutex/pthread_cond或futex机制。核心数据结构Node 节点源码剖析AQS 内部通过静态内部类Node来构建双向 FIFO 队列。理解Node的各个成员变量及其状态值waitStatus是理解 AQS 行为的关键。以下是 OpenJDK8 中Node类的核心源码及系统级注释staticfinalclassNode{/** * 标识节点处于共享模式Shared。 * 多个线程可以同时获取同步状态如 Semaphore, CountDownLatch。 */staticfinalNodeSHAREDnewNode();/** * 标识节点处于独占模式Exclusive。 * 同一时刻只能有一个线程获取同步状态如 ReentrantLock。 */staticfinalNodeEXCLUSIVEnull;// waitStatus 状态常量定义 /** * 线程被取消Cancelled。 * 由于超时或中断线程不再参与锁的竞争需要从队列中剔除。 * 这是唯一一个大于 0 的状态值。 */staticfinalintCANCELLED1;/** * 后继节点需要被唤醒Signal。 * 表示当前节点的后继节点处于阻塞park状态。 * 当当前节点释放锁或被取消时必须唤醒unpark其后继节点。 */staticfinalintSIGNAL-1;/** * 线程在条件队列中等待Condition。 * 节点当前处于条件队列Condition Queue中正在等待某个条件变量。 * 当调用 Condition.signal() 后该节点会被转移到同步队列Sync Queue中。 */staticfinalintCONDITION-2;/** * 共享模式传播Propagate。 * 在共享模式下该状态保证释放信号能够不失真地传播到后续节点。 * 用于解决早期 JDK 版本中共享锁释放时可能出现的并发挂起问题。 */staticfinalintPROPAGATE-3;/** * 节点状态初始化为 0。 * 它的值只能是上述四个常量之一或者是 0表示初始化/新建状态。 * 使用 volatile 保证多线程可见性。 */volatileintwaitStatus;/** * 指向当前节点在同步队列中的前驱节点。 * 1. 用于在入队时进行 CAS 尾插校验。 * 2. 用于在节点取消CANCELLED时向前遍历跳过无效节点。 */volatileNodeprev;/** * 指向当前节点在同步队列中的后继节点。 * 释放锁时当前节点通过 next 指针找到需要被唤醒的线程。 */volatileNodenext;/** * 当前节点所代表的线程。 * 在构造 Node 时绑定在使用完毕后出队后置为 null便于 GC。 */volatileThreadthread;/** * 1. 在同步队列Sync Queue中表示下一个等待共享模式的节点。 * 2. 在条件队列Condition Queue中指向单向链表中的下一个等待节点。 */NodenextWaiter;/** * 判断当前节点是否处于共享模式 */finalbooleanisShared(){returnnextWaiterSHARED;}/** * 返回当前节点的前驱节点如果为 null 则抛出空指针异常。 */finalNodepredecessor()throwsNullPointerException{Nodepprev;if(pnull)thrownewNullPointerException();elsereturnp;}Node(){// 用于初始化 Head 节点或者创建 SHARED 占位标记}Node(Threadthread,Nodemode){// 用于同步队列this.nextWaitermode;this.threadthread;}Node(Threadthread,intwaitStatus){// 用于条件队列this.waitStatuswaitStatus;this.threadthread;}}独占锁模式获取与释放源码深剖在独占模式Exclusive下同一时刻只允许一个线程持有锁。其核心入口为acquire(int arg)与release(int arg)。1. 独占锁获取acquire流程剖析AQS 的acquire方法是一个经典的模板方法其整体执行逻辑如下publicfinalvoidacquire(intarg){// 1. tryAcquire(arg) 尝试获取锁此方法由具体子类如 ReentrantLock实现。// 2. 如果获取失败调用 addWaiter 将当前线程包装为 EXCLUSIVE 节点插入队列尾部。// 3. 调用 acquireQueued 自旋尝试获取锁若获取不到则在队列中阻塞。// 4. 若在阻塞过程中线程被中断过acquireQueued 会返回 true最后调用 selfInterrupt 补中断信号。if(!tryAcquire(arg)acquireQueued(addWaiter(Node.EXCLUSIVE),arg))selfInterrupt();}快速入队与兜底入队addWaiter源码当获取锁失败后需要将线程插入队列尾部。AQS 设计了快速尝试插入与自旋保证成功enq的双重保障机制。privateNodeaddWaiter(Nodemode){// 将当前线程与模式EXCLUSIVE/SHARED包装成新节点NodenodenewNode(Thread.currentThread(),mode);// 尝试进行快速入队Fast-pathNodepredtail;if(pred!null){node.prevpred;// 采用 CAS 将尾指针 tail 指向当前 nodeif(compareAndSetTail(pred,node)){pred.nextnode;// 双向关联完成快速插入returnnode;}}// 快速入队失败存在并发竞争或队列未初始化进入自旋入队enq(node);returnnode;}privateNodeenq(finalNodenode){// 死循环自旋确保节点最终必定能成功插入队列for(;;){Nodettail;if(tnull){// 必须进行初始化// 队列为空通过 CAS 创建一个空节点作为 Head 哨兵节点if(compareAndSetHead(newNode()))tailhead;// 尾指针也指向该哨兵节点继续自旋}else{node.prevt;// 再次尝试 CAS 尾插if(compareAndSetTail(t,node)){t.nextnode;returnt;// 成功插入返回前驱节点退出循环}}}}队列内自旋与阻塞挂起acquireQueued源码节点进入队列后并非立即阻塞。如果该节点的前驱是head说明它是排在最前面的节点会再次尝试获取锁。否则将判断是否需要挂起。finalbooleanacquireQueued(finalNodenode,intarg){booleanfailedtrue;// 异常标志防范异常退出导致无法取消节点try{booleaninterruptedfalse;// 是否被中断过for(;;){finalNodepnode.predecessor();// 获取当前节点的前驱// 如果前驱是 head说明当前节点是队列中第一个有效节点有资格尝试获取锁if(pheadtryAcquire(arg)){setHead(node);// 获取成功将当前节点设为 head其 thread 和 prev 会被置空p.nextnull;// 帮助 GC 垃圾回收旧 headfailedfalse;returninterrupted;// 返回在等待过程中是否被中断过}// 走到这里说明前驱不是 head或者 tryAcquire 抢锁失败。// 1. 判断当前线程是否应该被挂起park// 2. 如果应该挂起调用 parkAndCheckInterrupt 挂起当前线程并等待唤醒if(shouldParkAfterFailedAcquire(p,node)parkAndCheckInterrupt())interruptedtrue;// 标记曾被中断过}}finally{if(failed)cancelAcquire(node);// 异常时将节点状态标记为 CANCELLED 并剥离队列}}挂起条件判定shouldParkAfterFailedAcquire源码AQS 判定一个线程是否可以安心挂起的依据是**它的前驱节点的waitStatus是否为SIGNAL**。如果是SIGNAL说明前驱释放锁时会负责通知自己此时当前线程便可安全挂起。privatestaticbooleanshouldParkAfterFailedAcquire(Nodepred,Nodenode){intwspred.waitStatus;if(wsNode.SIGNAL)/* * 前驱节点状态已经是 SIGNAL。 * 这意味着前驱释放时会唤醒我所以当前节点可以安全地被阻塞。 */returntrue;if(ws0){/* * ws 0 只能是 CANCELLED。 * 说明前驱节点已被取消我们需要跨过它向前寻找一个有效的节点。 */do{node.prevpredpred.prev;}while(pred.waitStatus0);pred.nextnode;// 建立新的双向连接剔除被取消的节点}else{/* * ws 此时只能是 0 或者 PROPAGATE。 * 通过 CAS 将前驱的状态设置为 SIGNAL指示前驱释放锁时通知我。 * 此处不挂起返回 false让外层循环再自旋尝试抢一次锁Double Check。 */compareAndSetWaitStatus(pred,ws,Node.SIGNAL);}returnfalse;}线程系统级挂起parkAndCheckInterrupt源码privatefinalbooleanparkAndCheckInterrupt(){// 调用底层 Unsafe 类的 park 方法使当前线程进入 WAITING 状态。// 该方法会引发 OS 级别的线程上下文切换释放 CPU 时间片。LockSupport.park(this);// 当线程被 unpark 唤醒或被中断interrupt唤醒时会继续向下执行。// 返回并清除当前线程的中断状态。returnThread.interrupted();}2. 独占锁释放release流程剖析释放锁的过程相对简单核心任务是修改state状态并唤醒unpark队列中的第一个有效后继节点。publicfinalbooleanrelease(intarg){// tryRelease 由子类如 ReentrantLock实现修改 state 并判断是否完全释放锁if(tryRelease(arg)){Nodehhead;// 如果头节点不为空且状态不为 0说明后面有排队等待唤醒的节点if(h!nullh.waitStatus!0)unparkSuccessor(h);// 唤醒后继节点returntrue;}returnfalse;}唤醒后继节点unparkSuccessor源码privatevoidunparkSuccessor(Nodenode){/* * 此时 node 一般是 head。如果它的状态小于 0如 SIGNAL * 尝试用 CAS 将其重置为 0允许其他并发操作例如新获取锁的线程重新设置它。 */intwsnode.waitStatus;if(ws0)compareAndSetWaitStatus(node,ws,0);/* * 获取要唤醒的后继节点。 * 正常情况下是 node.next但如果 node.next 被取消CANCELLED或者是 null * 我们必须从尾部tail向前遍历寻找最靠前的那个有效非取消节点。 */Nodesnode.next;if(snull||s.waitStatus0){snull;// 从尾部 tail 开始向前搜索直到找到最靠近 head 且 waitStatus 0 的有效节点for(Nodettail;t!nullt!node;tt.prev)if(t.waitStatus0)st;}if(s!null)// 唤醒该节点封装的系统级线程LockSupport.unpark(s.thread);}系统工程师深度思考为什么在后继节点失效时AQS 必须从尾部向前遍历而不是从首部向后遍历核心原因在于addWaiter阶段非原子性的指针关联步骤。观察addWaiter插入尾部时的代码node.prev pred;第一步前驱指针关联compareAndSetTail(pred, node)第二步原子 CAS 变更尾指针pred.next node;第三步后继指针关联在多线程极高并发下可能第 2 步成功但第 3 步尚未执行。此时如果从head向后遍历依靠next指针会在半路遇到next null的断链情况导致无法访问到新插入的尾节点。而node.prev pred是在 CAS 之前完成的因此从尾部向前遍历依靠prev指针能保证完整、无遗漏地遍历整条队列。共享锁模式获取、传播与释放源码深剖共享锁Shared允许多个线程同时持有如Semaphore和CountDownLatch。其核心机制在于一旦某个节点成功获取了共享锁它会立刻尝试将这种“成功状态”向后传播Propagate连续唤醒后续的共享节点。1. 共享锁获取与传播acquireSharedsetHeadAndPropagatepublicfinalvoidacquireShared(intarg){// tryAcquireShared 返回负数表示失败0 表示成功但无剩余共享资源正数表示成功且有剩余资源if(tryAcquireShared(arg)0)doAcquireShared(arg);// 进入共享获取等待队列}privatevoiddoAcquireShared(intarg){// 将当前线程包装为 SHARED 节点插入同步队列尾部finalNodenodeaddWaiter(Node.SHARED);booleanfailedtrue;try{booleaninterruptedfalse;for(;;){finalNodepnode.predecessor();if(phead){intrtryAcquireShared(arg);if(r0){// 获取成功将当前节点设为 head并向下传播唤醒信号setHeadAndPropagate(node,r);p.nextnull;// help GCif(interrupted)selfInterrupt();failedfalse;return;}}if(shouldParkAfterFailedAcquire(p,node)parkAndCheckInterrupt())interruptedtrue;}}finally{if(failed)cancelAcquire(node);}}传播链的关键setHeadAndPropagate源码该方法是共享锁设计的精髓解决了“唤醒信号不间断传递”的问题。privatevoidsetHeadAndPropagate(Nodenode,intpropagate){Nodehhead;// 记录旧的 headsetHead(node);// 将当前成功获取锁的节点设为新 head/* * 满足以下条件之一就需要唤醒后继节点 * 1. propagate 0表示还有剩余共享资源。 * 2. 旧 head 为 null基本不可能或者旧 head 的 waitStatus 0可能是 SIGNAL 或 PROPAGATE。 * 3. 新 head 为 null基本不可能或者新 head 的 waitStatus 0。 */if(propagate0||hnull||h.waitStatus0||(hhead)null||h.waitStatus0){Nodesnode.next;// 只有在后继节点是共享模式SHARED或者为空时才执行释放if(snull||s.isShared())doReleaseShared();}}2. 共享锁释放的核心doReleaseShared源码doReleaseShared负责在共享模式下安全地释放并向下传递唤醒信号。privatevoiddoReleaseShared(){/* * 这是一个死循环因为在唤醒后继节点的同时可能会有新的共享节点加入 * 或者是其他线程在并发释放。 */for(;;){Nodehhead;if(h!nullh!tail){intwsh.waitStatus;if(wsNode.SIGNAL){// 如果 head 状态为 SIGNAL说明后面有需要唤醒的节点。// 必须通过 CAS 将状态重置为 0。若失败说明有并发竞争重新自旋。if(!compareAndSetWaitStatus(h,Node.SIGNAL,0))continue;// loop to recheck casesunparkSuccessor(h);// 唤醒后继节点}elseif(ws0// 如果 head 状态已经是 0为了确保共享锁释放信号不丢失// 用 CAS 将其设为 PROPAGATE确保后续节点在 setHeadAndPropagate 中能感知到!compareAndSetWaitStatus(h,0,Node.PROPAGATE))continue;// CAS 失败说明 head 被改变了重试}if(hhead)// 如果在上述操作中 head 没有改变说明释放链完成退出break;}}系统工程师深度思考为什么引入Node.PROPAGATE状态JDK8 对 JDK 6/7 的重要修复在 JDK 6 时期AQS 的共享模式没有PROPAGATE状态。在某些高并发场景下如果一个线程释放共享锁调用doReleaseShared的同时另一个线程刚好获取锁并将其设为新head旧head的waitStatus会被直接重置为0并且没有机会修改成SIGNAL。这会导致获取锁的线程在调用setHeadAndPropagate时判断propagate 0且head.waitStatus 0从而不进行唤醒传播。这会导致队列里后面处于SHARED模式的线程永远处于park状态即著名的JDK-6801020 挂起 Bug。JDK8 引入了PROPAGATE。当waitStatus 0时通过 CAS 将其强行改为PROPAGATE (-3)。这样在接下来的setHeadAndPropagate判断中h.waitStatus 0依然成立从而强制触发doReleaseShared()唤醒后面的共享节点保证了并发通知的确定性。AQS 底层硬件级与 OS 级支持AQS 的高效与线程安全并非单凭 Java 代码实现而是严重依赖于 JVM 暴露的底层硬件与系统级原语。1. CAS 与 Memory Layout内存布局在 AQS 的末尾可以看到其通过sun.misc.Unsafe来直接操作内存偏移量privatestaticfinalUnsafeunsafeUnsafe.getUnsafe();privatestaticfinallongstateOffset;privatestaticfinallongheadOffset;privatestaticfinallongtailOffset;privatestaticfinallongwaitStatusOffset;privatestaticfinallongnextOffset;static{try{// 在类加载阶段通过 Unsafe 静态获取 AQS 内部属性在物理内存中的偏移地址OffsetstateOffsetunsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField(state));headOffsetunsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField(head));tailOffsetunsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField(tail));waitStatusOffsetunsafe.objectFieldOffset(Node.class.getDeclaredField(waitStatus));nextOffsetunsafe.objectFieldOffset(Node.class.getDeclaredField(next));}catch(ReflectiveOperationExceptione){thrownewError(e);}}// 基于 CPU 原语的 CAS 操作privatefinalbooleancompareAndSetTail(Nodeexpect,Nodeupdate){returnunsafe.compareAndSwapObject(this,tailOffset,expect,update);}在 x86 架构下unsafe.compareAndSwapObject对应底层汇编指令是lock cmpxchg。lock前缀会锁住系统总线或利用高速缓存一致性协议 MESI 锁定缓存行阻止其他 CPU 核心同时修改该内存区域。它同时提供了内存屏障Memory Barrier的效果禁止指令重排确保了 CAS 前后数据的可见性。2. LockSupport.park/unpark 的系统调用AQS 的阻塞与唤醒借助于LockSupport在 Linux 平台上HotSpot 虚拟机对LockSupport.park()的实现是基于Posix 线程库pthread。每个 Java 线程在 JVM 中都对应一个OSThread其内部持有一个Parker对象。Parker底层使用了pthread_mutex互斥锁和pthread_cond条件变量。当调用park时JVM 执行pthread_cond_wait将线程放入内核等待队列并让出 CPU。当调用unpark时JVM 执行pthread_cond_signal唤醒该线程使其重新参与 OS 的线程调度。总结AQS 的精妙之处在于将繁琐、高并发、易出错的“线程阻塞管理”、“队列竞态处理”、“内存可见性约束”高度抽象并下沉到 JVM 底层。它通过volatile state充当轻量级锁状态标志。双向 CLH 队列变体优雅地管理和剔除等待线程利用prev保证极端并发下入队的稳定性。共享模式下的PROPAGATE状态保障高并发唤醒信号不会因时序交错而丢失。这些底层的系统级考量共同构成了整个 Java 并发体系稳固的基石。
返回列表