文章内容收录到个人网站,方便阅读:hardyfish.top/
ReentrantLock 的线程安全,本质由两件事托底:互斥与内存可见性。
互斥保证同一时刻只有一个线程进入临界区(critical section,受保护的共享资源访问区);
内存可见性保证临界区内对共享变量的写入,在解锁后对随后获得锁的线程可见。
互斥:基于 AQS 的“独占状态”与 CAS 争用
ReentrantLock 的底层是 AQS(AbstractQueuedSynchronizer,同步器框架) 。AQS 用一个 state 整数表示同步状态,对独占锁而言通常含义是“是否被占用/占用次数”。
-
抢锁:线程通过 CAS(Compare-And-Swap,比较并交换)尝试把
state从0改为1(或从当前值递增)。- 成功:获得锁,成为 owner(持有者)。
- 失败:进入 AQS 的 CLH 队列(FIFO 等待队列),阻塞/自旋等待后续唤醒。
-
锁的排他性:只有 owner 才能继续执行临界区代码;非 owner 必须等待
state归零并被唤醒后再竞争。
这种设计的关键点在于:对 state 的修改使用原子操作(CAS),从而在高并发下也能可靠地裁决“谁赢得锁”。
可重入:用“持有者 + 计数”避免自我死锁
“可重入(reentrant)”指同一线程在持有锁时再次 lock() 不会被自己阻塞。
ReentrantLock 通过两类信息实现:
-
持有者线程记录:AQS 维护当前独占持有者(owner thread)。
-
重入次数计数:
state充当计数器。- 第一次获取锁:
state从0变为1,owner 设为当前线程。 - 同一线程再次获取:检测 owner 是自己,直接
state++。 - 释放锁:
state--;直到减到0才真正释放所有权并唤醒队列中的后继线程。
- 第一次获取锁:
因此,临界区内调用链较深、方法间层层加锁时,不会发生“自己把自己锁死”的问题。
阻塞与唤醒:队列化等待减少竞争风暴
当抢锁失败,线程不会无休止忙等,而是进入 AQS 队列并在合适时机被唤醒:
- 入队后,线程可能
park(挂起),释放 CPU。 - 解锁时,持有者把
state释放到0,再按策略唤醒队列中的后继节点竞争。
队列化的价值在于把竞争从“所有线程同时冲刺”变成“排队接力”,显著降低上下文切换与缓存抖动带来的损耗。
内存可见性:lock/unlock 的 happens-before 语义
线程安全不只需要“同一时刻只有一个线程进来”,还需要“前一个线程写过的数据,后一个线程读得到”。
ReentrantLock 提供与 synchronized 类似的内存语义:
- unlock 之前的写,在 随后某线程成功 lock 之后的读 中可见(happens-before)。
- 这依赖于 AQS 内部对
state等字段的 volatile 语义 与 CAS/park-unpark 等操作在 Java 内存模型下形成的屏障效果。
直观理解:解锁像“刷盘提交”,加锁像“重新加载最新版本”,保证共享变量不会读到旧值。
公平与非公平:安全性相同,调度策略不同
ReentrantLock 有两种策略:
- 非公平锁(默认) :新来的线程可能“插队”直接 CAS 抢到锁,吞吐通常更高。
- 公平锁:倾向于按队列先来后到获取锁,减少饥饿(starvation)风险。
两者都满足互斥与可见性要求,线程安全语义一致,差别在于获得锁的次序与性能特征。
关键用法约束:释放必须与获取配对
ReentrantLock 的线程安全依赖“锁的协议”被正确遵守:
- 释放次数必须与获取次数匹配(重入多少次就要 unlock 多少次),否则会造成锁长期不释放。
- 通常在
finally中释放,避免异常路径遗留锁导致其他线程永久阻塞。
这套机制合起来,使 ReentrantLock 在并发访问共享资源时,既能保证互斥,又能保证内存可见性,从而实现可验证的线程安全。