漫画图解java可重入锁(ReentrantLock)的实现原理

本文通过生动的故事比喻,深入浅出地讲解了Java中ReentrantLock的实现机制,包括公平锁与非公平锁的工作原理,以及与之相关的CAS、AQS等核心概念。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

转载自:https://blog.youkuaiyun.com/yanyan19880509/article/details/52345422/

前言

相信学过java的人都知道 synchronized 这个关键词,也知道它用于控制多线程对并发资源的安全访问,兴许,你还用过Lock相关的功能,但你可能从来没有想过java中的锁底层的机制是怎么实现的。如果真是这样,而且你有兴趣了解,今天我将带领你轻松的学习下java中非常重要,也非常基础的可重入锁-ReentrantLock的实现机制。


听故事把知识掌握了

在一个村子里面,有一口井水,水质非常的好,村民们都想打井里的水。这井只有一口,村里的人那么多,所以得出个打水的规则才行。村长绞尽脑汁,最终想出了一个比较合理的方案,咱们来仔细的看看聪明的村长大人的智慧。

井边安排一个看井人,维护打水的秩序。

打水时,以家庭为单位,哪个家庭任何人先到井边,就可以先打水,而且如果一个家庭占到了打水权,其家人这时候过来打水不用排队。而那些没有抢占到打水权的人,一个一个挨着在井边排成一队,先到的排在前面。打水示意图如下 :

打水示意图

是不是感觉很和谐,如果打水的人打完了,他会跟看井人报告,看井人会让第二个人接着打水。这样大家总都能够打到水。是不是看起来挺公平的,先到的人先打水,当然不是绝对公平的,自己看看下面这个场景 :

同家人一起打水

看着,一个有娃的父亲正在打水,他的娃也到井边了,所以女凭父贵直接排到最前面打水,羡煞旁人了。 
以上这个故事模型就是所谓的公平锁模型,当一个人想到井边打水,而现在打水的人又不是自家人,这时候就得乖乖在队列后面排队。

事情总不是那么一帆风顺的,总会有些人想走捷径,话说看井人年纪大了,有时候,眼力不是很好,这时候,人们开始打起了新主意。新来打水的人,他们看到有人排队打水的时候,他们不会那么乖巧的就排到最后面去排队,反之,他们会看看现在有没有人正在打水,如果有人在打水,没辄了,只好排到队列最后面,但如果这时候前面打水的人刚刚打完水,正在交接中,排在队头的人还没有完成交接工作,这时候,新来的人可以尝试抢打水权,如果抢到了,呵呵,其他人也只能睁一只眼闭一只眼,因为大家都默认这个规则了。这就是所谓的非公平锁模型。新来的人不一定总得乖乖排队,这也就造成了原来队列中排队的人可能要等很久很久。 
 

java可重入锁-ReentrantLock实现细节

ReentrantLock支持两种获取锁的方式,一种是公平模型,一种是非公平模型。在继续之前,咱们先把故事元素转换为程序元素。

元素转换 
 

咱们先来说说公平锁模型:

初始化时, state=0,表示无人抢占了打水权。这时候,村民A来打水(A线程请求锁),占了打水权,把state+1,如下所示:

线程A获取锁

线程A取得了锁,把 state原子性+1,这时候state被改为1,A线程继续执行其他任务,然后来了村民B也想打水(线程B请求锁),线程B无法获取锁,生成节点进行排队,如下图所示:

线程B等待

初始化的时候,会生成一个空的头节点,然后才是B线程节点,这时候,如果线程A又请求锁,是否需要排队?答案当然是否定的,否则就直接死锁了。当A再次请求锁,就相当于是打水期间,同一家人也来打水了,是有特权的,这时候的状态如下图所示:

可重入锁获取

到了这里,相信大家应该明白了什么是可重入锁了吧。就是一个线程在获取了锁之后,再次去获取了同一个锁,这时候仅仅是把状态值进行累加。如果线程A释放了一次锁,就成这样了:

线程A释放一次锁

仅仅是把状态值减了,只有线程A把此锁全部释放了,状态值减到0了,其他线程才有机会获取锁。当A把锁完全释放后,state恢复为0,然后会通知队列唤醒B线程节点,使B可以再次竞争锁。当然,如果B线程后面还有C线程,C线程继续休眠,除非B执行完了,通知了C线程。注意,当一个线程节点被唤醒然后取得了锁,对应节点会从队列中删除。 
 

非公平锁模型

如果你已经明白了前面讲的公平锁模型,那么非公平锁模型也就非常容易理解了。当线程A执行完之后,要唤醒线程B是需要时间的,而且线程B醒来后还要再次竞争锁,所以如果在切换过程当中,来了一个线程C,那么线程C是有可能获取到锁的,如果C获取到了锁,B就只能继续乖乖休眠了。这里就不再画图说明了。 
 

其它知识点

java5中添加了一个并发包, java.util.concurrent,里面提供了各种并发的工具类,通过此工具包,可以在java当中实现功能非常强大的多线程并发操作。对于每个java攻城狮,我觉得非常有必要了解这个包的功能。虽然做不到一步到位,但慢慢虚心学习,沉下心来,总能慢慢领悟到java多线程编程的精华。 
 

结束语

可重入锁的实现会涉及到CAS,AQS,java内存可见性(volatile)等知识,为了避免大家直接被代码搞晕,故而想以最简单的方式把可重入锁进行抽象,讲明白其中的实现原理,这样看起源码也有个借鉴的思路,希望本篇能够帮助到你们。

<think>我们正在讨论Java中的ReentrantLock,特别是它的可重入机制是如何实现的。可重入性指的是同一个线程可以多次获得同一把,而不会导致死ReentrantLock通过一个计数器(state)来记录的重入次数。 根据引用[1],[2],[3],[4]和[5],ReentrantLock可重入性是基于AQS(AbstractQueuedSynchronizer)实现的。AQS内部维护了一个状态变量(state)和一个记录当前持有的线程(exclusiveOwnerThread)。当线程第一次获取时,状态state被设置为1,同时记录当前持有的线程。如果同一个线程再次获取,状态值就会增加(例如,state=2)。每次释放时,状态值减1,直到状态值为0时,才完全释放,其他线程才能获取。 引用[5]展示了非公平的nonfairTryAcquire方法,该方法中: - 如果当前状态为0(未被持有),则尝试通过CAS设置状态为acquires(通常为1)来获取。 - 如果当前状态不为0,但当前线程是持有的线程(exclusiveOwnerThread),则将状态增加acquires(即重入次数增加),并返回true表示获取成功。 因此,可重入机制的关键在于:当线程尝试获取时,如果发现当前持有的线程就是自己,那么就直接增加状态值(重入计数),而不是阻塞。 下面我们详细解释可重入实现原理:</think>### Java ReentrantLock 可重入机制实现原理 ReentrantLock可重入性是其核心特性,允许同一线程多次获取同一把而不会死。其实现基于 **AQS(AbstractQueuedSynchronizer)框架**,通过状态变量和线程持有者记录实现重入计数。以下是关键实现原理: --- #### 一、核心数据结构 1. **状态变量 `state`** 在 AQS 中维护一个整型变量 `state`: - `state = 0`:未被任何线程持有 - `state = N`($N > 0$):被重入了 $N$ 次 $$ \text{state} = \begin{cases} 0 & \text{空闲} \\ k & \text{被同一线程重入 } k \text{ 次} \end{cases} $$ 2. **独占线程记录 `exclusiveOwnerThread`** 保存当前持有的线程引用,用于判断重入请求是否来自同一线程。 --- #### 二、重入获取流程 以非公平的 `nonfairTryAcquire()` 方法为例(引用[5]): ```java final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); // 获取当前状态值 // 情况1:未被占用 if (c == 0) { if (compareAndSetState(0, acquires)) { // CAS原子操作 setExclusiveOwnerThread(current); // 设置当前线程为持有者 return true; } } // 情况2:已被占用,但占用者是当前线程(重入) else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; // 重入次数累加 if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); // 直接更新状态(无需CAS) return true; } return false; // 获取失败 } ``` 1. **首次获取** - 当 `state=0` 时,通过 CAS 操作将 `state` 设置为 1($0 \rightarrow 1$) - 记录当前线程为持有者 2. **重入获取** - 检测到当前线程已是持有者(`current == exclusiveOwnerThread`) - 直接执行 `state = state + acquires`(通常 acquires=1) - **无需 CAS** 操作(因线程独占,无竞争) --- #### 三、重入释放流程 ```java protected final boolean tryRelease(int releases) { int c = getState() - releases; // 计算释放后的状态 if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); // 非持有线程禁止释放 boolean free = false; if (c == 0) { // 完全释放 free = true; setExclusiveOwnerThread(null); // 清除持有者 } setState(c); // 更新状态 return free; } ``` 1. **部分释放** - 每次 `unlock()` 将 `state` 减 1($k \rightarrow k-1$) - 当 $k > 1$ 时,仍被当前线程持有 2. **完全释放** - 当 `state=0` 时,清除线程持有者标记 - 唤醒等待队列中的其他线程 --- #### 四、可重入机制图解 ``` 线程A首次获取: state: 0 → 1 exclusiveOwnerThread: null → ThreadA 线程A第二次获取: state: 1 → 2 (重入计数+1) 线程A第一次释放: state: 2 → 1 线程A第二次释放: state: 1 → 0 exclusiveOwnerThread: ThreadA → null (完全释放) ``` --- #### 五、技术优势 1. **避免自死** 允许递归调用或嵌套同步方法(如 `methodA()` 调用 `methodB()`,两者需同一)[^1][^4]。 2. **状态精确跟踪** 通过 `state` 精确记录重入深度,确保释放次数必须匹配获取次数[^2]。 3. **性能优化** 重入时直接更新状态(无 CAS 竞争),降低开销[^5]。 > 该机制是 ReentrantLock 区别于简单互斥的核心,也是 Java 并发包中设计的基石[^3][^4]。 --- ### 典型重入场景示例 ```java ReentrantLock lock = new ReentrantLock(); void methodA() { lock.lock(); // 首次获取(state=1) try { methodB(); // 嵌套调用 } finally { lock.unlock(); // 释放(state=0) } } void methodB() { lock.lock(); // 重入获取(state=2) try { // 操作共享资源 } finally { lock.unlock(); // 释放(state=1) } } ```
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值