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:
| 能力 | synchronized | ReentrantLock + Condition |
|---|---|---|
| 锁释放自动 | 是 | 否(finally unlock) |
| 可重入 | 是 | 是 |
| 中断响应 | 否 | 是 |
| 超时等待 | 否 | tryLock(3, SECONDS) |
| 条件变量 | 1 个 | 多个 Condition |
| 公平性 | 非公平 | 可配置 |
七、总结
- AQS 三件套:state + 双向等待队列 + CAS,是所有同步工具的地基。
- 公平/非公平:公平 = 入队前看队列;非公平 = 先 CAS 抢一次。入队后都 FIFO。
- 获取流程:tryAcquire → 入队 → 自旋检查前驱是 head → park。
- 释放流程:state 减到 0 → unpark 后继。
- 重入:state 就是重入计数,减到 0 才真正释放。
- Condition:每个 Condition 一条等待队列,精准唤醒。
下一篇:synchronized 和 volatile 的底层原理,JMM 三大特性是怎么落到 CPU 缓存上的。