好的,我将为你撰写一篇关于ReentrantLock实现原理的技术文档,按照你要求的三个方面进行分析,并采用诙谐幽默的比喻和实战例子来增强可读性。主要内容如下:
- ReentrantLock是什么:介绍ReentrantLock的定义和核心特点,使用生活化的比喻解释其基本概念。
- ReentrantLock的核心特性:通过表格对比和代码示例详细说明可重入性、公平性等核心特性。
- ReentrantLock的实现原理:深入剖析AQS框架、加锁解锁过程以及公平/非公平锁的实现差异,附带源码分析。
接下来,我将开始撰写文章正文。
揭秘ReentrantLock:Java并发编程中的超级锁利器
1 什么是ReentrantLock?:从厕所排队说起
想象一下这样的场景:一个热门商场的厕所,很多人同时想使用,但每次只能进去一个人。这种"独占"使用厕所的场景,在并发编程中就是典型的互斥访问问题。Java中的ReentrantLock(可重入锁)就是解决这类问题的一种高效工具,它提供了比传统的synchronized关键字更强大、更灵活的锁机制。
官方定义:ReentrantLock是Java并发包(java.util.concurrent.locks)中的一种可重入互斥锁,它具有与synchronized相同的并发性和内存语义,但增加了更多高级功能。
简单来说,ReentrantLock就像是一个智能门禁系统,它有三个核心特点:
- 互斥性:像厕所门锁一样,一次只允许一个线程进入
- 可重入性:如同你有权限重复进入同一个厕所(虽然比喻有点奇怪),同一线程可以多次获取同一把锁
- 灵活性:提供公平性选择、可中断的锁获取、超时机制等高级功能
与synchronized相比,ReentrantLock就像是功能更全面的"高级门禁系统"。synchronized是Java内置的关键字,使用简单但功能有限;而ReentrantLock是一个完整的类,提供了更多精细控制能力。
基本用法先睹为快:
ReentrantLocklock=newReentrantLock();// 创建非公平锁// ReentrantLock lock = new ReentrantLock(true); // 创建公平锁publicvoidcriticalSection(){lock.lock();// 获取锁try{// 临界区代码 - 你的宝贵资源访问在这里System.out.println("线程"+Thread.currentThread().getName()+"正在操作共享资源");}finally{lock.unlock();// 必须确保释放锁}}注意:lock.unlock()必须放在finally块中,确保即使发生异常也能释放锁,避免死锁。
理解了ReentrantLock的基本概念后,接下来我们看看它到底有哪些令人惊艳的特性,让它成为并发编程中的"明星组件"。
2 ReentrantLock的核心特性:不只是个锁那么简单
如果把synchronized比作一把普通门锁,那么ReentrantLock就是一把智能指纹锁,它提供了丰富多样的高级功能,满足各种复杂场景的需求。让我们通过一个对比表格直观感受两者的区别:
表:ReentrantLock与synchronized特性对比
| 特性 | ReentrantLock | synchronized |
|---|---|---|
| 实现层面 | API层面(JUC包) | JVM层面(关键字) |
| 锁的获取 | 可尝试、可定时、可中断 | 只能阻塞等待 |
| 公平性 | 可选公平锁或非公平锁 | 只有非公平锁 |
| 条件队列 | 可绑定多个Condition | 只有一个等待池 |
| 释放保证 | 必须手动在finally中unlock() | 自动释放,由JVM保证 |
2.1 可重入性:递归调用的"通行证"
可重入性是ReentrantLock的核心特性之一。想象一下,你进入一个房间后,发现里面还有个内门需要同一把钥匙打开。可重入锁就允许你用同一把钥匙打开内门,而不会被自己挡在门外。
在技术层面,可重入意味着:同一线程可以多次获取同一把锁而不会被阻塞。这对于递归调用或者多个方法需要同一把锁的场景至关重要。
publicclassRecursiveExample{privatefinalReentrantLocklock=newReentrantLock();publicvoidouter(){lock.lock();// 第一次获取锁try{inner();// 调用需要同一把锁的方法System.out.println("外部方法执行,锁重入次数: "+lock.getHoldCount());// 查看重入次数}finally{lock.unlock();}}publicvoidinner(){lock.lock();// 第二次获取同一把锁(重入)try{// 一些操作System.out.println("内部方法执行,当前重入次数: "+lock.getHoldCount());}finally{lock.unlock();}}}如果没有可重入性,当线程在inner()方法中尝试获取锁时,会因为自己已经持有锁而被阻塞,导致死锁。而ReentrantLock通过内部计数器跟踪重入次数,每次lock()时计数器加1,每次unlock()时计数器减1,直到计数器为0时锁才真正释放。
2.2 公平性与非公平性:排队还是插队?
ReentrantLock提供了公平锁和非公平锁两种模式,这体现了它在锁策略上的灵活性。
- 公平锁(
new ReentrantLock(true)):像银行排队一样,先来后到,保证等待时间最长的线程优先获取锁 - 非公平锁(
new ReentrantLock(false),默认):像高峰期挤地铁,允许插队,新来的线程可能比先等待的线程先拿到锁
性能权衡:公平锁保证了公平性,但性能较低(线程切换频繁);非公平锁虽然不公平,但吞吐量更高。在大多数场景下,非公平锁是更好的选择,因为它能减少线程切换的开销。
2.3 尝试锁与可中断:灵活的资源获取策略
ReentrantLock提供了多种灵活的锁获取方式,避免线程无限期阻塞:
尝试锁(tryLock):像等电梯时设定时间限制,如果等太久就走楼梯
publicbooleantryIncrement(longtimeout,TimeUnitunit){try{if(lock.tryLock(timeout,unit)){// 尝试在指定时间内获取锁try{// 在指定时间内成功获取锁,执行操作counter++;returntrue;}finally{lock.unlock();}}else{// 超时未获取锁,执行备用方案System.out.println("获取锁超时,执行备用逻辑");returnfalse;}}catch(InterruptedExceptione){Thread.currentThread().interrupt();returnfalse;}}可中断锁:在等待锁的过程中可以响应中断请求,像排队时接到重要电话可以暂时离开。
publicvoidinterruptibleLock(){try{lock.lockInterruptibly();// 可中断地获取锁try{// 执行操作while(!Thread.currentThread().isInterrupted()){// 检查中断状态}}finally{lock.unlock();}}catch(InterruptedExceptione){// 处理中断,优雅退出System.out.println("锁获取被中断,优雅退出");Thread.currentThread().interrupt();}}2.4 条件变量:精细化的线程协调机制
synchronized与wait()/notify()配合使用,但只能有一个等待条件;而ReentrantLock可以创建多个条件变量(Condition),实现更精细的线程协调。
在生产者-消费者模型中,这一特性特别有用:
publicclassBoundedBuffer<T>{privatefinalReentrantLocklock=newReentrantLock();privatefinalConditionnotFull=lock.newCondition();// 非满条件privatefinalConditionnotEmpty=lock.newCondition();// 非空条件privatefinalObject[]items=newObject[100];privateintputptr,takeptr,count;publicvoidput(Tx)throwsInterruptedException{lock.lock();try{while(count==items.length){notFull.await();// 等待"非满"条件}items[putptr]=x;if(++putptr==items.length)putptr=0;++count;notEmpty.signal();// 通知"非空"条件已满足}finally{lock.unlock();}}publicTtake()throwsInterruptedException{lock.lock();try{while(count==0){notEmpty.await();// 等待"非空"条件}Tx=(T)items[takeptr];if(++takeptr==items.length)takeptr=0;--count;notFull.signal();// 通知"非满"条件已满足returnx;}finally{lock.unlock();}}}通过使用不同的Condition,我们可以精确控制哪些线程被唤醒,避免synchronized中notifyAll()带来的"惊群效应"。
了解了这些强大特性后,你可能好奇:ReentrantLock是如何在底层实现这些功能的呢?接下来我们就深入其核心实现原理。
3 ReentrantLock的实现原理:深入AQS核心
要理解ReentrantLock的工作原理,我们需要先认识它的基石:AQS(AbstractQueuedSynchronizer),即抽象队列同步器。AQS是Java并发包的核心框架,ReentrantLock的所有功能都建立在AQS之上。
3.1 AQS:并发框架的核心引擎
AQS可以看作是一个同步状态的管理器,它内部维护了三个关键组件:
state(状态字段):
volatile int类型变量,表示锁的状态- 对于
ReentrantLock,state = 0表示锁未被占用 state > 0表示锁被占用,数值表示重入次数
- 对于
独占线程:记录当前持有锁的线程
CLH队列:一个虚拟的双向队列,用于管理等待锁的线程
AQS使用了模板方法模式,它定义了获取锁和释放锁的骨架,而具体的获取/释放逻辑则由子类实现。这种设计让AQS成为了一个强大的同步框架。
3.2 加锁过程剖析:以非公平锁为例
当我们调用lock.lock()时,背后发生了什么?让我们以默认的非公平锁为例深入分析:
// NonfairSync的加锁过程finalvoidlock(){if(compareAndSetState(0,1)){// 1. 首先尝试CAS快速获取锁setExclusiveOwnerThread(Thread.currentThread());// 成功:设置当前线程为独占者}else{acquire(1);// 2. 失败:进入AQS获取流程}}// AQS的acquire方法publicfinalvoidacquire(intarg){if(!tryAcquire(arg)&&// 3. 再次尝试获取锁acquireQueued(addWaiter(Node.EXCLUSIVE),arg))// 4. 失败后加入队列并阻塞selfInterrupt();}这个过程可以类比为医院挂号的场景:
- 直接尝试(插队):新来的患者(线程)先不看排队情况,直接问挂号窗口:“现在能挂吗?”(CAS操作)
- 快速成功:如果恰好没人挂号(
state = 0),直接成功,避免排队开销 - 正式排队:如果窗口有人(
state ≠ 0),则乖乖去排队(进入CLH队列) - 队列中等待:在队列中耐心等待,轮到自已时再次尝试
非公平锁的tryAcquire实现:
// NonfairSync的tryAcquire实现protectedfinalbooleantryAcquire(intacquires){finalThreadcurrent=Thread.currentThread();intc=getState();// 获取当前状态if(c==0){// 情况1:锁未被占用if(compareAndSetState(0,acquires)){// CAS尝试获取setExclusiveOwnerThread(current);returntrue;}}elseif(current==getExclusiveOwnerThread()){// 情况2:重入intnextc=c+acquires;// 增加重入次数if(nextc<0)// 溢出检查thrownewError("Maximum lock count exceeded");setState(nextc);returntrue;}returnfalse;// 获取失败}3.3 释放锁过程:唤醒后续等待者
释放锁的过程相对简单,主要工作是状态恢复和唤醒后继线程:
// ReentrantLock的unlock方法publicvoidunlock(){sync.release(1);// 委托给AQS的release方法}// AQS的release方法publicfinalbooleanrelease(intarg){if(tryRelease(arg)){// 尝试释放Nodeh=head;if(h!=null&&h.waitStatus!=0)unparkSuccessor(h);// 唤醒队列中的下一个线程returntrue;}returnfalse;}// Sync的tryRelease实现protectedfinalbooleantryRelease(intreleases){intc=getState()-releases;// 减少重入次数if(Thread.currentThread()!=getExclusiveOwnerThread())thrownewIllegalMonitorStateException();// 只有持有者能释放booleanfree=false;if(c==0){// 完全释放free=true;setExclusiveOwnerThread(null);}setState(c);returnfree;}释放过程的关键点是:只有当重入次数减到0时,锁才真正释放,此时才会唤醒等待队列中的线程。
3.4 公平锁 vs 非公平锁的实现差异
公平锁与非公平锁的核心区别体现在tryAcquire方法的实现上:
// 公平锁的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;}hasQueuedPredecessors()方法是公平性的守护者,它检查同步队列中是否有比当前线程等待时间更长的线程。如果有,当前线程就不能"插队",必须乖乖排队。
3.5 正确使用ReentrantLock的注意事项
虽然ReentrantLock功能强大,但使用不当会导致严重问题。以下是几个关键实践要点:
1. lock()必须在try外部调用
// 正确写法publicvoidcalculate(){lock.lock();// lock()在try外面try{// 临界区代码intresult=100/0;// 可能抛出异常}finally{lock.unlock();}}// 错误写法(可能导致异常信息被覆盖)publicvoidcalculate(){try{lock.lock();// 错误:lock()在try内部intresult=100/0;}finally{lock.unlock();}}2. 必须使用try-finally确保锁释放
publicvoidriskyMethod(){lock.lock();try{// 可能抛出异常的代码dangerousOperation();}finally{lock.unlock();// 保证无论发生什么,锁都会被释放}}3. 避免在lock()和try之间插入代码
publicvoidproblematicMethod(){lock.lock();intnum=1/0;// 危险:在加锁后、try之前可能抛出异常!try{// 临界区代码}finally{lock.unlock();}}遵循这些最佳实践,可以避免常见的陷阱,确保ReentrantLock的正确使用。
4 实战应用与总结
4.1 实战场景举例
场景1:高性能计数器
publicclassHighPerformanceCounter{privatefinalReentrantLocklock=newReentrantLock();privateintcount=0;publicvoidincrement(){lock.lock();try{count++;}finally{lock.unlock();}}// 使用tryLock实现非阻塞版本publicbooleantryIncrement(){if(lock.tryLock()){try{count++;returntrue;}finally{lock.unlock();}}returnfalse;}}场景2:简单的阻塞队列
publicclassSimpleBlockingQueue<T>{privatefinalQueue<T>queue=newLinkedList<>();privatefinalReentrantLocklock=newReentrantLock();privatefinalConditionnotEmpty=lock.newCondition();privatefinalConditionnotFull=lock.newCondition();privatefinalintcapacity;publicvoidput(Titem)throwsInterruptedException{lock.lock();try{while(queue.size()==capacity){notFull.await();// 等待"非满"条件}queue.offer(item);notEmpty.signal();// 通知"非空"条件}finally{lock.unlock();}}publicTtake()throwsInterruptedException{lock.lock();try{while(queue.isEmpty()){notEmpty.await();// 等待"非空"条件}Titem=queue.poll();notFull.signal();// 通知"非满"条件returnitem;}finally{lock.unlock();}}}4.2 总结与选型建议
ReentrantLock是Java并发编程中的重要工具,它基于AQS实现了高效、可重入的锁机制。通过分析源码,我们了解了:
- 全局结构:Sync、NonfairSync和FairSync的分工协作
- 核心逻辑:state管理锁状态,CAS确保原子性
- 生命周期:初次上锁依赖CAS,重入时更新state,释放时递减state
- 公平性:非公平锁高吞吐,公平锁防饥饿
选型建议:
- 首选synchronized:简单场景,不需要
ReentrantLock的高级功能时 - 需要高级功能时选择ReentrantLock:可中断、超时、公平锁、多个条件变量等复杂场景
- 谨慎使用公平锁:公平锁有性能开销,除非必要(如防止饥饿),否则使用非公平锁
- 确保正确释放:
unlock()必须放在finally块中,避免死锁
ReentrantLock提供了比synchronized更精细的锁控制,是处理复杂并发场景的利器。通过深入理解其实现原理,我们能够更好地利用这一强大工具,编写出高效、可靠的并发程序。
参考资料
- https://juejin.cn/post/7499317287724597299
- https://blog.csdn.net/weixin_45149504/article/details/152175150
- https://blog.csdn.net/majianxin1/article/details/102603380
- https://blog.csdn.net/weixin_39996605/article/details/148588071
- https://blog.csdn.net/2401_87398486/article/details/151581727
- https://cloud.tencent.com/developer/article/2298552
- https://blog.csdn.net/feiying101/article/details/138394427