ReentrantLock 到底怎么排队的?AQS 源码拆解
2026/8/22 21:57:31 网站建设 项目流程

synchronized用得好好的,为什么要用 ReentrantLock?"可中断、可超时、公平锁、多条件变量"这些名词背得滚瓜烂熟,但一问底层怎么实现的,就只剩"基于 AQS"五个字。

这篇把 ReentrantLock 和它背后的 AQS(AbstractQueuedSynchronizer)从源码层面拆开,看完你就知道锁到底是怎么排队的、公平和非公平差在哪、Condition 是怎么做到"精准唤醒"的。

一、AQS 是什么:一把锁的核心三件套

AQS 本质上是一个同步器框架,核心只有三样东西:

// 1. 状态位:volatile int state(0=没锁,>0=被持锁)privatevolatileintstate;// 2. CLH 变体双向队列(排队等锁的线程)staticfinalclassNode{volatileThreadthread;// 排队的线程volatileNodeprev;// 前驱volatileNodenext;// 后继volatileintwaitStatus;// 节点状态}// head 指向队首(持有锁或即将持有),tail 指向队尾privatetransientvolatileNodehead;privatetransientvolatileNodetail;

一句话:state 记录"锁被占了几次",队列记录"谁在等"。所有同步器(锁、信号量、栅栏)都复用这套骨架。

二、公平锁 vs 非公平锁:只差一行

ReentrantLock 默认非公平。看源码:

// 非公平锁加锁finalvoidlock(){if(compareAndSetState(0,1))// 来了就抢(不管队列里有没有人等)setExclusiveOwnerThread(Thread.currentThread());elseacquire(1);}// 公平锁加锁finalvoidlock(){acquire(1);}// acquire → tryAcquireprotectedfinalbooleantryAcquire(intacquires){finalThreadcurrent=Thread.currentThread();intc=getState();if(c==0){// 公平锁:必须队列为空才抢;非公平锁:直接抢if(!hasQueuedPredecessors()&&compareAndSetState(0,acquires)){setExclusiveOwnerThread(current);returntrue;}}...}

非公平:新线程来先 CAS 抢一次,抢不到才排队;公平:先看队列有没有人等,有人就老实排队。所以非公平吞吐高(减少上下文切换),但可能"插队",饥饿风险小但长期排队者可能晚一点拿到。

三、获取锁失败怎么办:acquire 全流程

publicfinalvoidacquire(intarg){if(!tryAcquire(arg)&&// 1. 尝试获取(公平/非公平差异在这)acquireQueued(addWaiter(Node.EXCLUSIVE),arg))// 2. 失败→入队→排队自旋Thread.currentThread().interrupt();}

拆解:

第一步:入队(addWaiter)

privateNodeaddWaiter(Nodemode){Nodenode=newNode(mode);// 包装当前线程Nodepred=tail;if(pred!=null){node.prev=pred;if(compareAndSetTail(pred,node)){// CAS 挂到队尾pred.next=node;returnnode;}}enq(node);// 队列为空或 CAS 失败,用自旋入队returnnode;}

第二步:排队等待(acquireQueued)

finalbooleanacquireQueued(finalNodenode,intarg){booleaninterrupted=false;for(;;){finalNodep=node.predecessor();if(p==head&&tryAcquire(arg)){// 前驱是队首 → 再抢一次setHead(node);// 拿到锁,自己变队首p.next=null;// 出队returninterrupted;}// 没拿到 → 挂起线程(park),等前驱唤醒if(shouldParkAfterFailedAcquire(p,node))interrupted|=parkAndCheckInterrupt();}}

核心:每个节点在自己的前驱是 head时才有资格尝试获取锁——这就是队列的 FIFO 特性(非公平也只在入队前插队一次,入队后就老实排队)。

四、释放锁:唤醒后继

publicfinalbooleanrelease(intarg){if(tryRelease(arg)){// state 减到 0 才算释放Nodeh=head;if(h!=null){LockSupport.unpark(h.next.thread);// 唤醒队首的下一个}returntrue;}returnfalse;}

关键tryRelease里 state 减到 0 才真正释放(可重入时 state > 0 只是减一,锁还在)。唤醒的永远是head 的下一个节点

五、可重入:state 是怎么玩出花的

// 可重入:同一线程再次 lockprotectedfinalbooleantryAcquire(intacquires){Threadcurrent=Thread.currentThread();intc=getState();if(c==0){...}// 无人持有 → 直接拿elseif(current==getExclusiveOwnerThread()){// 自己已持有intnextc=c+acquires;// state + 1setState(nextc);// 重入+1returntrue;}returnfalse;// 别人持有 → 失败}

state 就是重入计数:0=无锁,1=持有一层,2=同一线程重入两次……每次 unlock 减一,减到 0 才真正释放唤醒后继。

六、Condition:精准唤醒怎么实现

synchronized只有 wait/notify 一个等待集;ReentrantLock 可以 new 多个 Condition,每个 Condition 一条独立等待队列

Locklock=newReentrantLock();ConditionnotFull=lock.newCondition();ConditionnotEmpty=lock.newCondition();
  • condition.await():当前线程释放锁,加入该 condition 的等待队列,挂起
  • condition.signal():把等待队列队首一个节点移到锁的同步队列,等它重新竞争
  • signalAll():全部移到同步队列

对比 synchronized:

能力synchronizedReentrantLock + Condition
锁释放自动否(finally unlock)
可重入
中断响应
超时等待tryLock(3, SECONDS)
条件变量1 个多个 Condition
公平性非公平可配置

七、总结

  1. AQS 三件套:state + 双向等待队列 + CAS,是所有同步工具的地基。
  2. 公平/非公平:公平 = 入队前看队列;非公平 = 先 CAS 抢一次。入队后都 FIFO。
  3. 获取流程:tryAcquire → 入队 → 自旋检查前驱是 head → park。
  4. 释放流程:state 减到 0 → unpark 后继。
  5. 重入:state 就是重入计数,减到 0 才真正释放。
  6. Condition:每个 Condition 一条等待队列,精准唤醒。

下一篇:synchronized 和 volatile 的底层原理,JMM 三大特性是怎么落到 CPU 缓存上的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询