news 2026/2/7 5:20:19

ReentrantLock(重入锁)基于AQS的源码实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ReentrantLock(重入锁)基于AQS的源码实现

一、先搞懂核心基础:ReentrantLock和AQS的关系

你可以把AQS(AbstractQueuedSynchronizer)理解成一个“同步组件的通用骨架”——它提前写好了同步队列管理、线程阻塞/唤醒的核心逻辑,只留了几个“钩子方法”(比如tryAcquiretryRelease)让子类自己实现。



一、钩子方法的通俗理解

钩子方法的核心可以用一个生活化的比喻:
你去餐厅吃“套餐”,餐厅(父类)已经定好了套餐的固定流程:先上开胃菜 → 上主菜 → 上甜品。但“主菜选什么”(比如选牛排还是烤鱼)这个步骤,餐厅留了一个“钩子”让你自己选——流程不变,但关键细节由你定制。

对应到编程中:

钩子方法是模板方法模式的核心,父类(通常是抽象类)定义了一个算法的整体骨架(模板方法),其中把算法中“需要子类定制的步骤”设计成空方法/抽象方法(这就是钩子方法),子类通过重写这些钩子方法,就能在不改变整体流程的前提下,定制算法的具体细节。

二、钩子方法的正式定义

钩子方法(Hook Method):

  • 父类声明(可以是抽象方法、空实现方法,甚至带默认实现的方法);
  • 父类的“模板方法”会调用这些钩子方法,但不关心钩子方法的具体实现;
  • 子类通过重写钩子方法,实现自己的业务逻辑,从而“钩入”父类的固定流程中。
三、结合AQS的例子(你最熟悉的场景)

这是理解钩子方法最好的例子,因为AQS就是模板方法模式的经典应用:

1. AQS的“模板方法”(固定流程)

AQS定义了加锁/解锁的整体流程(模板方法),比如:

  • 加锁流程:acquire(int arg)→ 先调用tryAcquire→ 失败则入队 → 自旋/阻塞;
  • 解锁流程:release(int arg)→ 先调用tryRelease→ 成功则唤醒队列线程。

这些acquirerelease是AQS的模板方法,流程完全固定,子类不能改。

2. AQS的“钩子方法”(子类定制)

AQS把“抢锁/释放锁的具体逻辑”设计成钩子方法,留给子类(比如ReentrantLock的FairSync/NonfairSync)实现:

AQS的钩子方法作用AQS的默认实现子类(ReentrantLock)的定制实现
tryAcquire(int)尝试获取独占锁直接抛UnsupportedOperationException公平锁:排队抢锁;非公平锁:允许插队抢锁
tryRelease(int)尝试释放独占锁直接抛异常减少state重入次数,清空锁持有者
isHeldExclusively()判断当前线程是否持有独占锁返回false判断exclusiveOwnerThread是否为当前线程
3. 具体例子:tryAcquire

AQS中tryAcquire的默认实现(钩子方法):

// AQS中的钩子方法(抽象模板里的“空钩子”)protectedbooleantryAcquire(intarg){thrownewUnsupportedOperationException();}

ReentrantLock的NonfairSync重写这个钩子方法(定制抢锁逻辑):

// 非公平锁定制的钩子方法实现protectedfinalbooleantryAcquire(intacquires){returnnonfairTryAcquire(acquires);// 非公平抢锁逻辑}

👉 核心逻辑:AQS的acquire流程不变,但tryAcquire的具体逻辑由ReentrantLock的子类决定——这就是钩子方法的核心价值。

四、钩子方法的核心特点
  1. 父类控流程,子类填细节:父类的模板方法定死了“做什么步骤”,子类的钩子方法决定“步骤怎么做”;
  2. 灵活性高:同一套父类流程,不同子类重写不同钩子方法,就能实现不同功能(比如AQS既可以实现ReentrantLock,也能实现CountDownLatch);
  3. 访问修饰符:钩子方法通常是protected(便于子类重写),模板方法通常是public final(防止子类修改流程)。
五、钩子方法 vs 普通重写方法

很多新手会混淆,核心区别:

维度钩子方法普通重写方法
调用方父类的模板方法主动调用通常由外部直接调用
作用定制父类流程中的某个步骤完全替换父类方法的逻辑
与父类的耦合度强耦合(依赖父类的流程)弱耦合(仅继承关系)
总结
  1. 钩子方法的核心:父类定“流程骨架”,子类通过重写钩子方法定制“流程中的细节”,是模板方法模式的核心;
  2. AQS中的典型应用tryAcquire/tryRelease是钩子方法,acquire/release是模板方法,ReentrantLock通过重写钩子方法实现公平/非公平锁;
  3. 本质价值:在不改变整体流程的前提下,让子类灵活定制核心逻辑,兼顾流程统一和细节灵活。

结合你之前看的ReentrantLock源码,钩子方法就是AQS留给FairSync/NonfairSync“定制抢锁逻辑”的入口,这也是为什么ReentrantLock能同时支持公平和非公平锁——只需要重写tryAcquire这个钩子方法即可。



ReentrantLock(重入锁)就是基于这个骨架做的“定制化产品”:

  • ReentrantLock内部有两个核心子类:FairSync(公平锁)、NonfairSync(非公平锁),它们都继承自AQS;
  • 你创建ReentrantLock时传true就是公平锁,传false就是非公平锁,本质是初始化不同的sync对象(FairSync/NonfairSync);
  • ReentrantLock是“独占锁”——同一时间只有一个线程能持有锁。

二、AQS的核心设计:支撑ReentrantLock的底层结构

AQS的核心是“同步队列+状态管理”,先把这两个基础搞懂,才能理解加锁解锁的源码。

1. 同步队列:线程排队的“双向链表”

当线程抢锁失败时,会被封装成一个“结点(Node)”加入队列等待,这个队列是带头尾指针的双向链表

  • head:队列头结点(代表当前正在持有锁的线程);
  • tail:队列尾结点(新抢锁失败的线程会加到尾部);
  • 队列遵循FIFO(先进先出)——正常情况下先排队的线程先抢锁。
2. Node结点:排队线程的“身份证”

每个抢锁失败的线程都会被包装成一个Node对象,里面存了关键信息:

属性作用
prev指向前一个结点(双向链表的“前向指针”)
next指向后一个结点(双向链表的“后向指针”)
thread当前结点对应的线程(比如线程A抢锁失败,这里就存A)
waitStatus结点状态(ReentrantLock中主要用3个值):
0:初始化状态
-1(SIGNAL):当前结点的前驱释放锁后,要唤醒当前结点的线程
1(CANCELLED):线程等待超时/被中断,放弃抢锁
3. state:锁的“重入次数计数器”

AQS里有个volatile int state(加了volatile保证可见性),在ReentrantLock里它的含义是“锁被线程持有的重入次数”:

  • state=0:锁是空闲的,没有线程持有;
  • state=1:锁被某个线程持有(未重入);
  • state>1:锁被同一个线程多次重入(比如线程A拿到锁后,又调用了一次lock(),state就变成2)。
4. exclusiveOwnerThread:锁的“持有者标记”

这个属性存在AQS的父类里,专门用来标记“当前哪个线程持有独占锁”——比如线程A拿到锁,这个属性就存A;A释放锁后,这个属性清空。

三、非公平锁的加锁流程(重点!实际开发用得最多)

我们以你笔记里的测试代码为入口,一步步拆解lock.lock()的底层逻辑:

ReentrantLocklock=newReentrantLock(false);// 非公平锁lock.lock();// 核心加锁方法
步骤1:NonfairSync的lock()方法——第一次“插队”抢锁

非公平锁的lock()方法会先做一个“快速抢锁”:

// NonfairSync的lock()核心逻辑finalvoidlock(){// 第一步:CAS尝试把state从0改成1(只有锁空闲时能成功)if(compareAndSetState(0,1))// 抢锁成功:标记当前线程为锁的持有者setExclusiveOwnerThread(Thread.currentThread());else// 抢锁失败:走AQS的acquire方法acquire(1);}

👉 这里的“非公平”体现:哪怕队列里有线程在排队,新线程也会先试试能不能直接抢锁,不按排队顺序。

步骤2:acquire(1)——抢锁失败后的“正式流程”

acquire是AQS的模板方法,核心逻辑如下(伪代码):

publicfinalvoidacquire(intarg){// 三个核心方法:tryAcquire(再抢一次锁)、addWaiter(加队列)、acquireQueued(阻塞等待)if(!tryAcquire(arg)&&addWaiter(Node.EXCLUSIVE)!=null&&acquireQueued(addWaiter(Node.EXCLUSIVE),arg)){// 处理中断(非核心,暂时忽略)}}

我们拆解这三个核心方法:

子步骤2.1:tryAcquire(1)——第二次“插队”抢锁

tryAcquire是AQS的钩子方法,NonfairSync重写了它,底层调用nonfairTryAcquire(1),这是“通用抢锁逻辑”,覆盖所有场景:

finalbooleannonfairTryAcquire(intacquires){finalThreadcurrent=Thread.currentThread();intc=getState();// 获取当前锁的状态// 场景1:锁空闲(state=0)if(c==0){// 再CAS抢一次锁(第二次插队机会)if(compareAndSetState(0,acquires)){setExclusiveOwnerThread(current);returntrue;// 抢锁成功}}// 场景2:锁已经被当前线程持有(重入)elseif(current==getExclusiveOwnerThread()){intnextc=c+acquires;// 重入次数+1(比如state从1变2)setState(nextc);returntrue;// 重入成功}// 场景3:锁被其他线程持有returnfalse;// 抢锁失败}

👉 为什么lock()里先做一次CAS,又在tryAcquire里做一次?—— 为了性能!对“锁空闲”这个高频场景做提前处理,减少后续复杂逻辑的调用。

子步骤2.2:addWaiter(Node.EXCLUSIVE)——抢锁失败,线程入队

如果tryAcquire失败,线程会被封装成Node结点,加入同步队列尾部。这个方法的核心是enq(node)(保证线程安全入队):

privateNodeaddWaiter(Nodemode){// 1. 创建新Node,封装当前线程(独占模式)Nodenode=newNode(Thread.currentThread(),mode);// 2. 先尝试快速入队(性能优化)Nodepred=tail;if(pred!=null){node.prev=pred;if(compareAndSetTail(pred,node)){pred.next=node;returnnode;}}// 3. 快速入队失败(比如队列为空/有竞争),走enq方法enq(node);returnnode;}// 核心:无锁化保证线程安全入队(CAS+循环)privateNodeenq(finalNodenode){for(;;){// 死循环,直到入队成功Nodet=tail;// 场景1:队列为空(head/tail都是null)if(t==null){// CAS创建空的头结点(这个结点不存线程,只是“占位符”)if(compareAndSetHead(newNode()))tail=head;// 尾指针也指向头结点}// 场景2:队列非空else{node.prev=t;// CAS把尾指针改成当前结点if(compareAndSetTail(t,node)){t.next=node;// 原尾结点的next指向当前结点returnt;}}}}

👉 关键理解:

  • 队列为空时,先创建一个“空的头结点”(没有线程),再把当前线程的Node加到尾部;
  • 用“CAS+死循环”实现无锁化入队——不用锁也能保证多线程下结点安全加到尾部;
  • 快速入队是性能优化,避免每次都走循环。
子步骤2.3:acquireQueued(Node, arg)——入队后,线程阻塞等待

线程入队后,不会立刻阻塞,而是进入“自旋抢锁+阻塞”的循环:

finalbooleanacquireQueued(finalNodenode,intarg){booleanfailed=true;try{booleaninterrupted=false;for(;;){// 死循环,直到抢锁成功finalNodep=node.predecessor();// 获取前驱结点// 场景1:前驱是头结点(说明轮到自己抢锁了)if(p==head&&tryAcquire(arg)){setHead(node);// 把当前结点设为新的头结点p.next=null;// 原头结点出队(垃圾回收)failed=false;returninterrupted;// 抢锁成功,退出循环}// 场景2:前驱不是头结点,或抢锁失败——准备阻塞if(shouldParkAfterFailedAcquire(p,node)&&parkAndCheckInterrupt()){interrupted=true;}}}finally{if(failed)cancelAcquire(node);}}

这里又拆两个核心方法:

① shouldParkAfterFailedAcquire:确保自己能被唤醒

线程阻塞前,必须确保“前驱结点会唤醒自己”,否则阻塞后永远醒不过来:

privatestaticbooleanshouldParkAfterFailedAcquire(Nodepred,Nodenode){intws=pred.waitStatus;// 获取前驱结点的状态// 场景1:前驱状态是SIGNAL(-1)——前驱释放锁后会唤醒当前线程if(ws==Node.SIGNAL)returntrue;// 可以放心阻塞// 场景2:前驱状态是CANCELLED(1)——前驱放弃抢锁了if(ws>0){// 往前找,直到找到一个状态不是CANCELLED的结点do{node.prev=pred=pred.prev;}while(pred.waitStatus>0);pred.next=node;// 把当前结点挂到正常结点后面}// 场景3:前驱状态是0(初始化)else{// CAS把前驱状态改成SIGNAL(-1)compareAndSetWaitStatus(pred,ws,Node.SIGNAL);}returnfalse;// 暂时不阻塞,继续循环}

👉 核心目的:确保当前结点的前驱状态是SIGNAL,这样前驱释放锁时会唤醒自己。

② parkAndCheckInterrupt:阻塞当前线程

如果shouldParkAfterFailedAcquire返回true,就调用这个方法阻塞线程:

privatefinalbooleanparkAndCheckInterrupt(){LockSupport.park(this);// 阻塞当前线程(线程进入WAITING状态)returnThread.interrupted();// 检查线程是否被中断(重置中断标记)}

👉LockSupport.park()是JDK底层的阻塞方法,比Object.wait()更灵活,不用依赖synchronized。

非公平锁加锁流程总结(一句话):

线程先两次“插队”抢锁(lock()的快速CAS + tryAcquire的通用CAS),都失败后封装成Node入队;入队后自旋检查是否轮到自己抢锁,没轮到就确保前驱会唤醒自己,然后阻塞;被唤醒后继续自旋抢锁,直到成功。

四、解锁流程:lock.unlock()

解锁比加锁简单,核心是“减少重入次数 + 唤醒队列里的线程”。

步骤1:unlock()调用release(1)
publicvoidunlock(){sync.release(1);// 调用AQS的release方法}publicfinalbooleanrelease(intarg){// 第一步:尝试释放锁if(tryRelease(arg)){Nodeh=head;// 第二步:队列非空,且头结点状态不是0(说明有线程在等待)if(h!=null&&h.waitStatus!=0)unparkSuccessor(h);// 唤醒后继线程returntrue;}returnfalse;}
步骤2:tryRelease(1)——释放锁(减少重入次数)

tryRelease是AQS的钩子方法,ReentrantLock重写了它:

protectedfinalbooleantryRelease(intreleases){intc=getState()-releases;// 重入次数-1(比如state从2变1)// 校验:只有持有锁的线程能释放锁if(Thread.currentThread()!=getExclusiveOwnerThread())thrownewIllegalMonitorStateException();booleanfree=false;// 场景1:重入次数减到0(完全释放锁)if(c==0){free=true;setExclusiveOwnerThread(null);// 清空锁的持有者}// 场景2:重入次数还没到0(只是减少一次重入)setState(c);returnfree;// 只有完全释放锁才返回true}

👉 为什么不用CAS?—— 因为只有持有锁的线程能调用unlock(),是单线程操作,不需要同步。

步骤3:unparkSuccessor(h)——唤醒队列里的线程

如果完全释放了锁,就唤醒队列里的下一个线程:

privatevoidunparkSuccessor(Nodenode){intws=node.waitStatus;// 把头结点状态重置为0if(ws<0)compareAndSetWaitStatus(node,ws,0);Nodes=node.next;// 头结点的后继结点// 场景1:后继结点被取消(状态=1),或为空if(s==null||s.waitStatus>0){s=null;// 从队列尾部往前找,找到离头结点最近的正常结点for(Nodet=tail;t!=null&&t!=node;t=t.prev)if(t.waitStatus<=0)s=t;}// 场景2:找到正常的后继结点,唤醒它if(s!=null)LockSupport.unpark(s.thread);// 唤醒线程}

👉 为什么从尾部往前找?—— 因为结点的next指针可能被修改(比如线程取消等待),而prev指针更稳定,能确保找到正确的线程。

五、非公平锁的“不公平”本质 + 为什么用它?

1. 不公平的原因

同步队列是FIFO(先进先出),正常情况下先排队的线程该先拿锁,但非公平锁允许“插队”:

  • 抢锁阶段:新线程在入队前有两次抢锁机会,哪怕队列里有线程在等;
  • 释放阶段:线程A释放锁时,先把state改成0(锁空闲),再去唤醒队列里的线程C;这中间的“时间差”里,新线程B可能直接抢锁成功,导致队列里的C继续等待。

👉 极端情况:队列里的线程可能一直抢不到锁(饥饿问题)。

2. 为什么实际开发多用非公平锁?

因为性能更高

  • 减少线程切换:如果新线程能直接抢到锁,不用入队、阻塞、唤醒,避免了线程上下文切换的开销;
  • 高并发下优势明显:虽然有饥饿风险,但实际业务中锁持有时间短,饥饿概率极低,性能收益远大于风险。

六、公平锁的核心差异(补充)

公平锁和非公平锁的核心区别只有两点:

  1. 公平锁的lock()方法没有“快速CAS抢锁”这一步,直接走acquire(1)
  2. 公平锁的tryAcquire方法里,除了判断state,还会检查“队列里有没有线程在等”——如果有,哪怕state=0,也不会抢锁,直接排队。

👉 公平锁严格按FIFO顺序抢锁,没有插队,但性能低(线程频繁阻塞/唤醒)。

总结(核心要点回顾)

  1. ReentrantLock的底层是AQS:AQS提供同步队列(双向链表)、state(重入次数)、线程阻塞/唤醒能力,ReentrantLock通过FairSync/NonfairSync重写AQS的钩子方法实现公平/非公平锁;
  2. 非公平锁加锁流程:两次插队抢锁(lock()快速CAS + tryAcquire通用CAS)→ 抢失败则入队 → 自旋检查前驱是否为头结点 → 确保能被唤醒后阻塞 → 被唤醒后继续自旋抢锁;
  3. 解锁流程:减少state重入次数 → 完全释放锁后(state=0)→ 从队列尾部往前找正常结点并唤醒;
  4. 非公平锁的取舍:牺牲“公平性”(可能饥饿)换取高性能,这是实际开发中选择它的核心原因;
  5. 同步队列的核心设计:用CAS+循环实现无锁化入队,Node的SIGNAL状态保证线程能被唤醒,FIFO特性是公平锁的基础。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/3 15:55:42

5分钟部署Z-Image-ComfyUI,文生图一键生成超清美图

5分钟部署Z-Image-ComfyUI&#xff0c;文生图一键生成超清美图 你是否试过输入一段文字&#xff0c;几秒后眼前就浮现出一张高清、细腻、风格精准的图片&#xff1f;不是模糊的草图&#xff0c;不是失真的构图&#xff0c;而是真正能用在海报、社交配图甚至设计初稿里的成品—…

作者头像 李华
网站建设 2026/2/6 10:39:10

人脸识别OOD模型实际作品:质量分分层抽样生成的特征空间分布热力图

人脸识别OOD模型实际作品&#xff1a;质量分分层抽样生成的特征空间分布热力图 1. 什么是人脸识别OOD模型&#xff1f; 你可能已经用过很多人脸识别系统——刷脸打卡、门禁通行、手机解锁。但有没有遇到过这些情况&#xff1a; 光线太暗时&#xff0c;系统反复提示“请正对镜…

作者头像 李华
网站建设 2026/2/5 6:05:34

GLM-4V-9B教育行业应用:数学题图解分析+物理实验图数据提取

GLM-4V-9B教育行业应用&#xff1a;数学题图解分析物理实验图数据提取 1. 为什么教育工作者需要一个“看得懂图”的AI&#xff1f; 你有没有遇到过这样的场景&#xff1a; 学生发来一张手写的数学几何题照片&#xff0c;辅助线画得歪歪扭扭&#xff0c;角度标注挤在角落&…

作者头像 李华
网站建设 2026/2/3 15:29:51

OFA视觉问答模型镜像:3步快速部署,零基础玩转图片问答

OFA视觉问答模型镜像&#xff1a;3步快速部署&#xff0c;零基础玩转图片问答 你有没有试过对着一张图发呆&#xff0c;心里想着“这图里到底在说什么”&#xff1f;或者刚拍完一张产品照&#xff0c;想立刻知道它在视觉上最抓人的点是什么&#xff1f;又或者&#xff0c;正帮…

作者头像 李华
网站建设 2026/2/6 7:48:50

零基础5分钟部署QwQ-32B:Ollama一键安装教程

零基础5分钟部署QwQ-32B&#xff1a;Ollama一键安装教程 你是不是也试过下载大模型&#xff0c;结果卡在“正在下载99%”、硬盘爆满、显存不足、环境报错……最后关掉终端&#xff0c;默默打开浏览器搜“还有没有更简单的方法”&#xff1f;别折腾了。今天这篇教程&#xff0c…

作者头像 李华
网站建设 2026/2/6 18:24:21

如何高效完成图片去背景?CV-UNet Universal Matting镜像开箱即用

如何高效完成图片去背景&#xff1f;CV-UNet Universal Matting镜像开箱即用 在电商运营、内容创作、设计协作等实际工作中&#xff0c;图片去背景&#xff08;抠图&#xff09;是高频刚需——商品主图需要纯白背景&#xff0c;海报设计需要透明元素&#xff0c;社交媒体配图需…

作者头像 李华