【面试专栏|Java并发编程】ReentrantLock源码拆解:可重入+公平/非公平锁
引言
在Java并发编程中,ReentrantLock是除synchronized外最常用的锁机制,它支持可重入、公平锁与非公平锁两种模式,灵活性远超synchronized。但很多开发者只停留在“会用”层面,对其底层原理、源码实现一知半解,面试时被追问就卡壳。本文将从源码出发,拆解ReentrantLock的可重入机制、公平锁与非公平锁的核心逻辑,搭配面试官高频追问,帮你吃透这一并发核心组件,面试、开发都能轻松应对~
文章目录
- 引言
- 一、REENTRANTLOCK 概述
- 二、可重入原理(源码拆解)
- 2.1 可重入的定义
- 2.2 核心源码拆解(lock()方法)
- 2.2.1 核心方法:tryAcquire(int arg)
- 2.2.2 可重入逻辑解读
- 2.2.3 unlock()方法(释放锁,配合可重入)
- 三、公平锁与非公平锁(源码对比)
- 3.1 构造方法(两种锁的初始化)
- 3.2 核心差异:tryAcquire()方法
- 3.2.1 非公平锁(NonfairSync)
- 3.2.2 公平锁(FairSync)
- 3.2.3 差异总结(表格对比)
- 3.3 公平锁与非公平锁的获取流程(mermaid图)
- 四、面试官追问环节(实战必备)
- 追问1:ReentrantLock的可重入性,和synchronized的可重入性有什么区别?
- 追问2:ReentrantLock为什么默认是非公平锁?
- 追问3:ReentrantLock和synchronized的核心区别是什么?(高频必问)
- 追问4:ReentrantLock的Condition是什么?怎么用?
- 五、总结
一、REENTRANTLOCK 概述
ReentrantLock是JUC(java.util.concurrent)包下的可重入互斥锁,基于AQS(AbstractQueuedSynchronizer)实现,核心优势在于:
- 支持可重入:同一线程可多次获取同一把锁,不会造成死锁;
- 支持两种锁模式:公平锁(FairLock)和非公平锁(NonfairLock),可根据场景选择;
- 灵活的锁操作:支持中断锁获取、超时获取锁,还能结合Condition实现精准通知/唤醒;
- 性能优越:在高并发场景下,竞争激烈时性能优于synchronized(JDK1.8后两者性能差距缩小,但ReentrantLock灵活性依然突出)。
小贴士:ReentrantLock默认使用非公平锁,若需使用公平锁,可通过构造方法传入true:new ReentrantLock(true)。建议点赞收藏,后续开发、面试直接查阅~
二、可重入原理(源码拆解)
2.1 可重入的定义
可重入锁,通俗来说就是“同一线程可以多次获取同一把锁”,比如线程A获取锁后,再次调用需要该锁的方法时,无需释放锁即可直接获取,避免死锁。
ReentrantLock的可重入性,核心依赖AQS的状态变量(state)和当前持有锁的线程(exclusiveOwnerThread)实现,我们直接看源码。
2.2 核心源码拆解(lock()方法)
ReentrantLock的lock()方法会委托给内部类Sync(继承AQS)的lock()方法,而Sync有两个子类:FairSync(公平锁)和NonfairSync(非公平锁),但可重入逻辑是两者共有的,核心在acquire()和tryAcquire()方法。
2.2.1 核心方法:tryAcquire(int arg)
tryAcquire()是AQS的核心方法,用于尝试获取独占锁,ReentrantLock通过重写该方法实现可重入,源码如下(以非公平锁为例,公平锁逻辑类似,差异在竞争锁的顺序):
// NonfairSync的tryAcquire方法protectedfinalbooleantryAcquire(intacquires){finalThreadcurrent=Thread.currentThread();intc=getState();// 获取AQS的状态变量state// 1. 若state为0,说明锁未被持有,尝试CAS获取锁if(c==0){if(compareAndSetState(0,acquires)){setExclusiveOwnerThread(current);// 设置当前线程为锁的持有者returntrue;}}// 2. 若锁已被持有,判断持有锁的线程是否是当前线程(可重入核心逻辑)elseif(current==getExclusiveOwnerThread()){intnextc=c+acquires;// 锁重入次数+1(acquires默认是1)if(nextc<0)// 溢出判断,避免重入次数过多导致异常thrownewError("Maximum lock count exceeded");setState(nextc);// 更新state状态returntrue;}// 3. 锁已被其他线程持有,获取失败returnfalse;}2.2.2 可重入逻辑解读
- state变量:AQS的state是一个volatile修饰的int变量,用于表示锁的持有状态:
- state = 0:锁未被任何线程持有;
- state > 0:锁被某一线程持有,state的值表示重入次数(每次重入+1,释放-1)。
- 可重入核心:当线程再次获取锁时,会判断当前持有锁的线程(exclusiveOwnerThread)是否是自己,若是,则直接将state+1,无需竞争锁,实现可重入。
- 锁释放:对应unlock()方法,每次释放会将state-1,直到state=0时,才会真正释放锁,让其他线程有机会获取。
2.2.3 unlock()方法(释放锁,配合可重入)
// ReentrantLock的unlock方法publicvoidunlock(){sync.release(1);}// AQS的release方法,委托给tryRelease()publicfinalbooleanrelease(intarg){if(tryRelease(arg)){Nodeh=head;if(h!=null&&h.waitStatus!=0)unparkSuccessor(h);returntrue;}returnfalse;}// ReentrantLock重写的tryRelease方法protectedfinalbooleantryRelease(intreleases){intc=getState()-releases;// 重入次数-1if(Thread.currentThread()!=getExclusiveOwnerThread())thrownewIllegalMonitorStateException();// 非持有锁线程释放锁,抛出异常booleanfree=false;if(c==0){// 重入次数为0,真正释放锁free=true;setExclusiveOwnerThread(null);// 清空持有锁的线程}setState(c);// 更新state状态returnfree;}关键提醒:ReentrantLock的unlock()方法必须与lock()方法成对出现,重入多少次,就必须释放多少次,否则会导致锁无法释放,造成死锁!建议在finally块中释放锁,确保异常情况下也能正确释放。
三、公平锁与非公平锁(源码对比)
ReentrantLock的公平锁和非公平锁,核心差异在于“获取锁的顺序”:公平锁会按照线程排队的顺序获取锁(FIFO),非公平锁则允许线程“插队”获取锁,效率更高,但可能导致线程饥饿。
3.1 构造方法(两种锁的初始化)
// 无参构造:默认非公平锁publicReentrantLock(){sync=newNonfairSync();}// 有参构造:true=公平锁,false=非公平锁publicReentrantLock(booleanfair){sync=fair?newFairSync():newNonfairSync();}3.2 核心差异:tryAcquire()方法
3.2.1 非公平锁(NonfairSync)
非公平锁的tryAcquire()方法,我们在2.2.1中已经看过,核心逻辑:
- 锁未被持有(state=0)时,直接CAS尝试获取锁,不考虑队列中排队的线程;
- 若锁已被持有,判断是否是当前线程,若是则重入。
简单说:非公平锁“不讲规矩”,只要锁空闲,不管队列里有没有线程排队,当前线程都能尝试抢锁,效率高,但可能导致排队久的线程一直抢不到锁(饥饿)。
3.2.2 公平锁(FairSync)
公平锁的tryAcquire()方法,在非公平锁的基础上,增加了“判断队列是否有等待线程”的逻辑:
// FairSync的tryAcquire方法protectedfinalbooleantryAcquire(intacquires){finalThreadcurrent=Thread.currentThread();intc=getState();if(c==0){// 关键差异:增加hasQueuedPredecessors()判断,是否有线程在队列中等待if(!hasQueuedPredecessors()&&compareAndSetState(0,acquires)){setExclusiveOwnerThread(current);returntrue;}}elseif(current==getExclusiveOwnerThread()){intnextc=c+acquires;if(nextc<0)thrownewError("Maximum lock count exceeded");setState(nextc);returntrue;}returnfalse;}// 核心方法:判断队列中是否有比当前线程更早等待的线程publicfinalbooleanhasQueuedPredecessors(){Nodet=tail;// 队列尾节点Nodeh=head;// 队列头节点Nodes;// 队列不为空 && 头节点不是当前线程 && 头节点的后继节点不为空 && 后继节点的线程不是当前线程returnh!=t&&((s=h.next)==null||s.thread!=Thread.currentThread());}3.2.3 差异总结(表格对比)
| 特性 | 公平锁(FairSync) | 非公平锁(NonfairSync) |
|---|---|---|
| 获取顺序 | 按线程排队顺序(FIFO) | 允许插队,优先当前线程抢锁 |
| 效率 | 较低(需判断队列,有上下文切换) | 较高(减少上下文切换) |
| 线程饥饿 | 不会出现(排队有序) | 可能出现(插队导致部分线程长期抢不到锁) |
| 适用场景 | 对公平性要求高、线程竞争不激烈的场景(如金融交易) | 高并发、对效率要求高的场景(如电商秒杀) |
3.3 公平锁与非公平锁的获取流程(mermaid图)
四、面试官追问环节(实战必备)
面试中,面试官在问完ReentrantLock核心原理后,大概率会追问以下问题,提前备好应答,避免卡壳!
追问1:ReentrantLock的可重入性,和synchronized的可重入性有什么区别?
答:两者都支持可重入,但实现方式和底层逻辑不同:
- 实现方式:
- ReentrantLock:基于AQS的state变量和exclusiveOwnerThread实现,重入时state+1,释放时state-1,手动控制锁的获取和释放;
- synchronized:基于JVM底层监视器锁(monitor)实现,自动实现可重入(JVM会记录线程的重入次数),无需手动释放。
- 灵活性:ReentrantLock的可重入可配合中断、超时等机制,更灵活;synchronized的可重入是自动的,灵活性较差。
追问2:ReentrantLock为什么默认是非公平锁?
答:核心是“平衡效率和公平性”:
- 非公平锁的效率更高:无需判断队列是否有等待线程,直接尝试抢锁,减少了线程上下文切换的开销(上下文切换会消耗CPU资源);
- 实际场景中,公平性需求较低:大多数业务场景(如电商、接口调用)更看重并发效率,而非绝对的公平性;
- 公平锁的弊端:即使锁空闲,线程也必须排队,会导致大量上下文切换,降低并发吞吐量。
追问3:ReentrantLock和synchronized的核心区别是什么?(高频必问)
答:核心区别体现在灵活性、性能、功能三个方面,总结如下:
| 维度 | ReentrantLock | synchronized |
|---|---|---|
| 实现层面 | 基于JDK(Java代码,AQS) | 基于JVM(底层C++实现) |
| 锁类型 | 可重入、公平/非公平锁(独占锁) | 可重入、非公平锁(独占锁) |
| 锁操作 | 手动lock()/unlock()(需finally释放) | 自动获取/释放(无需手动操作) |
| 功能扩展 | 支持中断、超时获取锁、Condition精准通知 | 不支持中断、超时,通知方式单一(notify()/notifyAll()) |
| 性能 | 高并发竞争激烈时,性能优于synchronized(JDK1.8后差距缩小) | JDK1.8前性能较差,1.8后引入偏向锁、轻量级锁,性能提升明显 |
追问4:ReentrantLock的Condition是什么?怎么用?
答:Condition是ReentrantLock的配套工具,用于实现“精准通知/唤醒”,替代synchronized的wait()/notify(),核心优势是可以创建多个Condition对象,实现不同线程的分别通知。
用法示例:
ReentrantLocklock=newReentrantLock();Conditioncondition1=lock.newCondition();// 条件1Conditioncondition2=lock.newCondition();// 条件2// 线程A等待条件1lock.lock();try{while(条件不满足){condition1.await();// 等待,释放锁}// 执行逻辑}finally{lock.unlock();}// 线程B唤醒等待条件1的线程lock.lock();try{// 满足条件,唤醒线程Acondition1.signal();// 唤醒单个线程// condition1.signalAll(); // 唤醒所有等待的线程}finally{lock.unlock();}五、总结
本文从源码出发,拆解了ReentrantLock的核心原理:
- 可重入性:基于AQS的state变量(重入次数)和exclusiveOwnerThread(锁持有者)实现,重入时state+1,释放时state-1;
- 公平锁与非公平锁:核心差异在tryAcquire()方法,公平锁需判断队列等待情况,非公平锁允许插队,效率更高;
- 实战价值:结合面试官高频追问,对比synchronized的差异、Condition的用法,帮你吃透面试重点。
ReentrantLock是Java并发编程的核心组件,掌握其底层源码和原理,不仅能应对面试,还能在实际开发中根据场景选择合适的锁策略,避免死锁、提升并发效率。
最后,觉得本文对你有帮助的话,点赞+收藏+关注哦!后续会持续拆解JUC核心组件,带你吃透并发编程,面试不踩坑~ 评论区说说你在使用ReentrantLock时遇到的问题吧!
