ReentrantLock(公平锁与非公平锁)的底层核心是基于 AQS(AbstractQueuedSynchronizer,抽象队列同步器) 实现,通过 AQS 的"状态管理"和"同步队列"两大核心机制,控制锁的获取、释放与重入逻辑。
一、底层核心依赖:AQS 的核心能力
AQS 是 ReentrantLock 实现的基础,它提供了两个关键支撑:
1.状态变量(state)
用 volatile int 修饰,记录锁的持有状态。
- 初始值为 0,表示锁未被持有;
- 线程获取锁时,CAS 将 state 从 0 改为 1;
- 支持重入:若当前线程已持有锁,再次获取时只需将 state 加 1;
- 释放锁时,state 减 1,直至减为 0 时锁完全释放。
2.同步队列(CLH 队列)
AQS(AbstractQueuedSynchronizer)的双向链表,本质是CLH(Craig, Landin, and Hagersten)锁队列的变种实现,用于存放竞争同步资源(如锁)失败的线程,核心作用是实现"线程阻塞等待"与"有序唤醒",是 AQS 保证同步逻辑的核心数据结构。
2.1、核心属性:链表的基础构成
java
AQS 双向链表通过两个关键成员变量维护链表头/尾,所有节点均为 Node 类型(AQS 的静态内部类):
private transient volatile Node head:链表头节点,代表"即将尝试获取资源的线程"(通常是已被唤醒、等待竞争资源的线程)。
private transient volatile Node tail:链表尾节点,新竞争失败的线程会作为新节点追加到尾部,保证"先进先出(FIFO)"的排队顺序。
Node 节点核心属性(每个节点对应一个竞争失败的线程):
volatile Thread thread:当前节点关联的线程。
volatile Node prev:前驱节点引用,实现双向链表的"向前遍历"(如节点取消时调整前驱指针)。
volatile Node next:后继节点引用,实现双向链表的"向后遍历"(如唤醒时找到下一个待唤醒的线程)。
volatile int waitStatus:节点状态,用于标记线程的等待状态,决定节点是否需要被唤醒或移除,核心状态包括:
0:初始状态,节点刚加入队列时的默认状态。
SIGNAL(-1):后继节点需要被唤醒(当前节点释放资源后,需主动唤醒后继节点)。
CANCELLED(1):节点对应的线程已取消等待(如调用 interrupt() 或超时),需从链表中移除。
CONDITION(-2):节点处于 Condition 条件队列中(非同步队列,用于 Condition.await()/signal())。
PROPAGATE(-3):仅用于共享模式(如 CountDownLatch),表示资源释放需向后传播唤醒所有后继节点。
2.2、核心功能:链表的核心操作逻辑
AQS 双向链表的操作围绕"节点入队""节点出队""节点取消"三大核心场景展开,所有操作均通过 CAS(Compare And Swap)保证线程安全(避免多线程操作链表时的并发问题)。
1. 节点入队:新线程竞争失败后加入队列
当线程尝试获取同步资源(如锁)失败时,会被封装为 Node 节点追加到链表尾部,流程如下:
scss
1.调用 addWaiter(Node mode) 方法,创建当前线程对应的 Node 节点(mode 区分独占/共享模式)。
2.通过 CAS 操作(compareAndSetTail(oldTail, newNode))将新节点设为新的 tail:
- 若 CAS 成功,直接将原尾节点的 next 指向新节点,完成入队。
- 若 CAS 失败(多线程并发入队),进入 enq(newNode) 方法循环重试,直至 CAS 成功(保证最终入队)。
3.入队后,调用 acquireQueued(Node node, int arg) 方法,将当前线程阻塞(通过 LockSupport.park()),等待被前驱节点唤醒。
2. 节点出队:线程被唤醒后竞争资源成功
当持有资源的线程释放资源(如 unlock())时,会唤醒链表头节点的后继节点,被唤醒的线程尝试竞争资源,成功后成为新的头节点(原头节点出队),流程如下:
scss
1.资源释放时,AQS 调用 unparkSuccessor(Node node) 方法,找到当前节点(通常是原头节点)的有效后继节点(排除 CANCELLED 状态的节点)。
2.通过 LockSupport.unpark(successor.thread) 唤醒后继节点对应的线程。
3.被唤醒的线程在 acquireQueued() 中再次尝试竞争资源(如 CAS 修改 AQS 的 state):
- 若竞争成功,将当前节点设为新的 head(原头节点的 prev 设为 null,thread 设为 null,便于 GC 回收),完成原头节点的出队。
- 若竞争失败(如非公平锁下被新线程插队),则继续阻塞,等待下一次唤醒。
3. 节点取消:线程放弃等待后移除节点
当线程等待超时或被中断时,会将自身对应的 Node 节点标记为 CANCELLED 状态,并从链表中移除,避免影响其他节点的唤醒逻辑,流程如下:
objectivec
1.线程在 acquireQueued() 中检测到中断或超时,调用 cancelAcquire(Node node) 方法,将节点状态设为 CANCELLED。
2.找到当前节点的前驱节点(跳过其他 CANCELLED 状态的节点),通过 CAS 将前驱节点的 next 指向当前节点的后继节点。
3.若当前节点是尾节点,通过 CAS 将 tail 设为前驱节点;若当前节点是头节点的后继节点,直接唤醒其后续节点,保证链表的连续性。
2.3、设计优势:双向链表的必要性
相比单向链表,双向链表的设计让 AQS 能更高效地处理节点取消和唤醒逻辑:
-
高效移除取消节点:通过 prev 指针可快速找到前驱节点,无需从表头遍历,避免单向链表"移除节点需遍历找前驱"的低效问题。
-
精准唤醒后继节点:通过 next 指针可直接定位后继节点,结合 waitStatus 状态,能快速筛选出有效节点(排除 CANCELLED 节点),避免无效唤醒。
2.4 总结
AQS 双向链表是"线程排队等待同步资源"的物理载体,通过 FIFO 排队顺序 保证同步的有序性(公平锁的核心依赖),通过 CAS 操作 保证多线程下链表操作的安全性,通过 waitStatus 状态管理 高效处理节点的唤醒与取消,最终支撑了 ReentrantLock、CountDownLatch 等同步工具的底层逻辑。
二、公平锁 vs 非公平锁的底层实现差异
二者底层均基于 AQS,但核心区别在于 "线程尝试获取锁时的判断逻辑",决定了是否遵循"先到先得"。
1. 非公平锁(默认,性能更优)
允许新线程"插队"竞争锁,不严格按排队顺序分配,底层逻辑如下:
获取锁(lock()):
objectivec
1.直接通过 CAS 尝试修改 AQS 的 state(0→1),若成功则直接持有锁,记录当前持有线程;
2.若 CAS 失败,判断当前线程是否已持有锁(重入检查):若是,state 加 1,直接重入;
3.若未持有锁,将当前线程封装为 Node 节点,加入 AQS 同步队列尾部,调用 LockSupport.park() 阻塞线程。
非公平性根源:新线程不检查队列是否有等待线程,直接竞争锁,可能"抢占"刚释放锁、尚未唤醒队列头线程的锁资源。
2. 公平锁(顺序性更优)
严格遵循"先到先得",新线程需先检查队列是否有等待线程,底层逻辑如下:
获取锁(lock()):
objectivec
1.先调用 hasQueuedPredecessors() 检查 AQS 同步队列:若队列有等待的 Node 节点(存在更早排队的线程),直接跳过竞争,将当前线程封装为 Node 加入队列尾部并阻塞;
2.若队列无等待线程,再通过 CAS 尝试修改 state(0→1),成功则持有锁;
3.若 CAS 失败,判断是否重入(state 加 1),否则加入队列阻塞。
公平性根源:hasQueuedPredecessors() 确保"只有队列无等待线程时,新线程才竞争锁",完全按线程入队顺序分配锁。
三、释放锁(unlock())的底层共性逻辑
公平锁与非公平锁的释放逻辑一致,均通过 AQS 完成:
scss
1.调用 tryRelease(int releases) 方法,将 AQS 的 state 减 1(releases 默认为 1);
2.若 state 减至 0,说明锁完全释放,此时会调用 unparkSuccessor(Node node) 唤醒 AQS 同步队列的"头节点的后继节点"(即最早排队的线程);
3.被唤醒的线程会再次尝试通过 CAS 竞争锁,重复上述"获取锁"逻辑。