Java并发(三):锁体系(分类、synchronized 与 Lock)
Java并发(三):锁体系(分类、synchronized 与 Lock)
导语:锁是并发面试的核心中的核心。本篇先把「到底有多少种锁」一次讲透——按阻塞策略、公平性、重入性、共享性、实现层次等维度建立分类坐标系,再逐层深入
synchronized的 Monitor 结构与锁升级、Lock 体系的 AQS 与读写锁。共 21 题。
一、锁的分类总览
1. 并发中的「锁」到底有多少种?如何分类?
答: Java 中的"锁"不是单一概念,而是从不同维度对同一批同步机制的分类。理解锁的第一步是建立分类坐标系,再对号入座——同一种锁可以同时属于多个类别。
| 分类维度 | 类型 | 含义 | 典型代表 |
|---|---|---|---|
| 阻塞策略 | 悲观锁 | 假设冲突必然发生,先加锁再操作 | synchronized、ReentrantLock |
| 乐观锁 | 假设冲突很少,提交时才校验 | CAS、AtomicInteger、数据库版本号 | |
| 线程等待方式 | 阻塞锁 | 获取不到就挂起线程 | synchronized 重量级锁 |
| 自旋锁 | 获取不到就循环 CAS 重试 | JVM 轻量级锁、Atomic* | |
| 无锁 / 无等待 | 不阻塞也不循环,每步必有线程完成 | ConcurrentLinkedQueue(无锁) | |
| 公平性 | 公平锁 | 严格按 FIFO 排队,先请求先得 | new ReentrantLock(true) |
| 非公平锁 | 允许插队抢锁,吞吐更高 | synchronized、ReentrantLock 默认 | |
| 可重入性 | 可重入锁 | 同一线程可重复获取同一把锁 | synchronized、ReentrantLock |
| 不可重入锁 | 重复获取会自我死锁 | StampedLock | |
| 共享性 | 独占锁(排他锁) | 同一时刻仅一个线程持有 | 写锁、ReentrantLock |
| 共享锁 | 多个线程可同时持有 | 读锁、Semaphore | |
| 读写分离 | 读写锁 | 读共享 / 写独占 | ReentrantReadWriteLock |
| 邮戳锁 | 支持乐观读,不可重入 | StampedLock | |
| 实现层次 | JVM 内置锁 | 由 JVM 实现,含偏向/轻量级/重量级三态 | synchronized |
| JDK API 锁 | 由 JDK 用 Java 代码 + AQS 实现 | ReentrantLock、Semaphore、CountDownLatch | |
| 锁粒度优化 | 分段锁 | 把一把大锁拆成多把细粒度锁 | JDK 7 ConcurrentHashMap 的 Segment |
| 偏向锁 | 无竞争时把锁"偏向"首个线程 | JDK 6~14 的 synchronized(已废弃) |
记忆技巧:悲观乐观看「策略」、公平非公平看「排队」、可重入看「重复获取」、独占共享看「并发度」、自旋阻塞看「等待方式」。
面试答题建议:被问到"有哪些锁"时,先抛出这张分类表,再挑 2~3 个展开讲清原理,远比零散罗列更有体系感。例如 ReentrantLock 默认就是「悲观 + 非公平 + 可重入 + 独占 + 轻微自旋后阻塞」的组合体。
2. 什么是悲观锁与乐观锁?Java 中如何实现?
答:
- 悲观锁:假设并发冲突一定会发生,操作前先加锁,期间其他线程阻塞等待。适合写多读少、冲突频繁的场景。
- Java 实现:
synchronized、ReentrantLock等独占锁。
- Java 实现:
- 乐观锁:假设冲突很少,不加锁,只在提交更新时校验数据是否被别人改过。
- Java 实现:CAS(
AtomicInteger、AtomicReference、LongAdder); - 数据库实现:版本号 / 时间戳字段(
UPDATE ... SET x = ? WHERE version = ?)。
- Java 实现:CAS(
| 维度 | 悲观锁 | 乐观锁 |
|---|---|---|
| 加锁时机 | 操作前 | 不加锁,提交时校验 |
| 并发冲突高时 | 表现稳定 | 大量自旋重试,浪费 CPU |
| 并发冲突低时 | 锁开销相对浪费 | 无锁,吞吐更高 |
| 风险 | 死锁、并发度低 | ABA 问题、自旋开销、只能保证单变量 |
易错提醒:
ReentrantLock虽然功能比synchronized更强,但本质仍是悲观互斥锁,获取不到同样会阻塞/等待,不是乐观锁。真正乐观的是 CAS 与Atomic*系列。语料中"Lock 是同步非阻塞、采用乐观并发策略"的说法是错误的。
3. 什么是可重入锁?为什么需要它?
答: 可重入锁指同一个线程可以多次获取同一把锁而不会死锁。实现方式通常是锁内部维护「持有线程 + 重入计数」:每获取一次计数 +1,每释放一次 -1,计数归零才真正释放锁。
synchronized:通过 Monitor 的_recursions字段记录重入次数;ReentrantLock:通过 AQS 的state字段记录(state加一/减一)。
为什么需要——如果锁不可重入,以下场景会自我死锁:
public synchronized void a() {
b(); // 同一线程再次进入同步方法
}
public synchronized void b() { }其他场景:递归加锁、子类同步方法调用父类同步方法、带锁的模板方法调用同样加锁的钩子方法。
反例:
StampedLock不可重入,同一线程重复获取会死锁,使用时必须格外小心。
4. 什么是自旋锁?自适应自旋锁又是什么?
答: 自旋锁指线程获取锁失败时不立即挂起,而是循环 CAS 重试一段时间,避免线程切换(用户态↔内核态)的开销。
- 适用:锁的持有时间极短的场景——自旋几十次就能拿到锁,比挂起再唤醒划算得多;
- 代价:自旋期间空耗 CPU。若锁被长时间持有,自旋就是纯粹的浪费,因此不能无限自旋。
自适应自旋(Adaptive Spinning) 是 HotSpot 的优化:自旋次数不固定,而由前一次在同一锁上的自旋成功率动态决定——
- 若上次自旋很快成功,JVM 认为这次也可能成功,允许更多次自旋;
- 若某锁很少自旋成功,则减少甚至跳过自旋,直接挂起。
自旋思想在 JDK 中随处可见:JVM 轻量级锁通过 CAS 自旋竞争;
Atomic*的incrementAndGet()内部就是自旋 CAS 循环;AQS 的入队也是 CAS 自旋。
5. 什么是独占锁与共享锁?
答:
| 类型 | 别名 | 并发性 | 典型代表 |
|---|---|---|---|
| 独占锁 | 排他锁、互斥锁 | 同一时刻只有 1 个线程能持有 | synchronized、ReentrantLock、ReentrantReadWriteLock.WriteLock |
| 共享锁 | 读锁 | 多个线程可同时持有 | ReentrantReadWriteLock.ReadLock、Semaphore |
在 AQS 中的体现:
- 独占模式:
tryAcquire/tryRelease(获取失败即入队阻塞); - 共享模式:
tryAcquireShared/tryReleaseShared(获取成功后还会唤醒后继节点,让它们也来尝试共享获取)。
关联考点:
CountDownLatch、Semaphore用的都是 AQS 的共享模式;ReentrantLock用独占模式;ReentrantReadWriteLock则同时使用两者——读锁走共享、写锁走独占。
6. 什么是公平锁与非公平锁?
答:
- 公平锁:严格按照等待队列的 FIFO 顺序授予锁,先请求先得到,不会出现饥饿;但每次都要判断队列、挂起与唤醒,性能较低(线程切换开销大)。
- 非公平锁:线程抢锁时先直接 CAS 尝试插队,成功就占有,失败才入队等待。吞吐量更高(减少线程挂起/唤醒次数),但可能导致某些线程长期饥饿。
| 维度 | 公平锁 | 非公平锁 |
|---|---|---|
| 获取顺序 | 严格 FIFO | 允许插队 |
| 吞吐量 | 低 | 高 |
| 饥饿风险 | 无 | 有 |
| 实现成本 | 每次都要检查队列 | 直接 CAS 抢,抢不到才排队 |
现实选择:绝大多数场景用非公平锁。因为"刚刚释放锁的线程紧接着又要获取锁"的情况很常见,此时插队能利用 CPU 缓存的局部性,避免上下文切换——牺牲一点公平性换取显著吞吐提升是划算的。
7. 什么是分段锁?
答: 分段锁(Segment Locking) 是一种锁粒度优化思想:把一把锁保护的大范围数据结构拆分成若干个独立的小段,每段用一把独立的锁,从而让不同段的操作可以真正并发。
- 经典实现:JDK 7 的
ConcurrentHashMap——内部是Segment[]数组,每个Segment继承ReentrantLock并各自维护一个HashEntry[]。默认 16 个段,理论上并发度可达 16,不同段可同时写。 - 优点:并发度从"1"提升到"段数";
- 缺点:分段有内存开销,且
size()这类全局操作需要遍历所有段、加锁重试,实现繁琐。
演进:JDK 8 的
ConcurrentHashMap放弃了分段锁,改用 CAS +synchronized锁单个桶头节点,锁粒度从"段"进一步细化到"桶",并发度不再固定为 16,同时支持红黑树。所以"分段锁"是 JDK 7 的标签,回答时务必区分版本。
二、synchronized 深入
8. synchronized 有哪些使用方式?对象锁与类锁的区别?
答:
| 使用方式 | 锁对象 | 说明 |
|---|---|---|
| 修饰实例方法 | 当前实例对象(this) | 不同实例之间互不影响 |
| 修饰静态方法 | 当前类的 Class 对象 | 即"类锁",全类共享一把 |
| 修饰代码块 | 括号中指定的对象 | 最灵活,支持任意对象作为锁 |
对象锁与类锁互不影响:实例方法锁的是 this,静态方法锁的是 MyClass.class,二者是不同的监视器,可以被不同线程同时持有。同一个类的所有静态同步方法则共享同一把类锁。
易错补充:
synchronized(String)要避免:字符串常量池会缓存相同字面量的String,不同代码可能意外共用同一把 String 锁,导致本不相关的逻辑被串行化甚至死锁。锁对象优先用new Object()或专用实例。synchronized修饰的方法,锁在方法调用时(运行时)才获取,而不是编译期。
9. synchronized 的底层实现原理是什么?
答: 分三个层面:
1)字节码层面
- 同步代码块:编译为
monitorenter与monitorexit指令,且编译器会保证异常路径上也有配对的monitorexit(这就是"异常也会释放锁"的原因); - 同步方法:不加指令,而是在方法访问标志中置
ACC_SYNCHRONIZED,由 JVM 在方法调用时隐式加解锁。
2)对象头层面:锁信息记录在对象的 Mark Word 中,重量级锁时指向一个 ObjectMonitor(见第 10 题)。
3)Monitor 结构层面(HotSpot 的 ObjectMonitor,C++ 实现):
| 字段 | 作用 |
|---|---|
_owner | 当前持有锁的线程 |
_recursions | 重入次数(这正是 synchronized 可重入的实现) |
_EntryList | 竞争锁失败而阻塞的线程队列(BLOCKED) |
_WaitSet | 调用 wait() 后主动让出锁、进入等待的线程集合(WAITING) |
_cxq | 最近竞争失败的线程入队的单向链表,与 _EntryList 配合 |
未获取到 Monitor 的线程会进入 _EntryList 并处于 BLOCKED 状态,涉及操作系统互斥量(mutex)与内核态切换,因此早期 synchronized 被称为"重量级锁"、开销很大;JDK 6 之后才做了大量优化。
区分「阻塞」与「等待」:
_EntryList是抢锁失败(BLOCKED),_WaitSet是主动wait()让出锁(WAITING)。被notify()唤醒后,线程从_WaitSet移回_EntryList重新抢锁——这也是wait/notify必须持有锁的原因之一。
10. Mark Word 与锁的四种状态是什么?
答: 对象头(Object Header)由 Mark Word + 类型指针(Klass Pointer) 组成。64 位 JVM 下 Mark Word 占 8 字节,它复用同一块内存存储不同信息,具体含义由最低 2~3 位的锁标志位决定:
| 锁状态 | 锁标志位 | Mark Word 存储内容 |
|---|---|---|
| 无锁 | 01 | 对象 hashCode(31 位)+ 分代年龄(4 位)+ 偏向标志 0 |
| 偏向锁 | 01 | 偏向线程 ID(54 位)+ Epoch(2 位)+ 分代年龄 + 偏向标志 1 |
| 轻量级锁 | 00 | 指向线程栈中 Lock Record 的指针 |
| 重量级锁 | 10 | 指向 Monitor(ObjectMonitor) 的指针 |
| GC 标记 | 11 | 空(不记录信息) |
四态演进方向是只能升级、不能降级(偏向锁撤销除外),因此也叫锁膨胀。
版本提醒(重要):偏向锁自 JDK 15 起被默认禁用(JEP 374),并在后续版本中已移除实现。因此现代 JDK 上实际只有「无锁 → 轻量级锁 → 重量级锁」三态。回答时务必说明版本,否则会被认为知识过时。
11. JDK 6 之后 synchronized 做了哪些锁升级优化?
答: HotSpot 引入锁升级(膨胀)机制,按竞争激烈程度逐级升级,提升无竞争/低竞争场景的性能:
| 级别 | 触发条件 | 实现方式 | 适用场景 |
|---|---|---|---|
| 偏向锁 | 只有一个线程访问 | Mark Word 记录线程 ID,连 CAS 都不需要 | 始终单线程访问 |
| 轻量级锁 | 出现第二个线程竞争 | CAS + 自旋 尝试获取,失败则自旋 | 竞争短暂、锁持有时间短 |
| 重量级锁 | 自旋失败(竞争激烈) | 进入 Monitor 阻塞队列,涉及内核态 | 竞争激烈、锁持有时间长 |
升级路径:无锁 → 偏向锁 → 轻量级锁 → 重量级锁。
重要修正(版本差异):偏向锁自 JDK 6 引入,但因维护成本高(撤销需要全局安全点、批量重偏向/撤销代价大)而在 JDK 15 中通过 JEP 374 被默认禁用并标记废弃,此后版本中实现已被移除。因此现代 JDK(17/21+)上,
synchronized实际是「无锁 → 轻量级锁 → 重量级锁」的直接升级。面试建议:讲偏向锁时必须补一句"JDK 15 起已禁用并移除",这恰恰是能体现你对版本演进敏感度的加分点;若只答"默认开启偏向锁",会被判定为知识陈旧。
12. synchronized 是可重入锁吗?
答: 是。同一个线程再次进入已持有锁的同步块时会成功获取,不会自我死锁。
实现上:Monitor 维护持有线程(_owner)与重入计数(_recursions)——线程每次进入同步块计数 +1,每次退出 -1,计数归零才真正释放锁并唤醒 _EntryList 中的后继线程。
ReentrantLock 的可重入则由 AQS 的 state 字段实现(见第 16 题),原理一致。
13. 什么是锁消除与锁粗化?
答: 二者都是 JIT 的锁优化手段,方向相反:
- 锁消除(Lock Elimination):JIT 借助逃逸分析判断某段代码的锁对象不可能被其他线程访问(即对象未逸出、是线程私有的),于是直接消除该锁,连加锁指令都不生成。
- 典型例子:局部变量上的
StringBuffer.append()——StringBuffer的方法都带synchronized,但对象是局部的,因此锁被消除。
- 典型例子:局部变量上的
- 锁粗化(Lock Coarsening):若一系列连续操作反复对同一个对象加锁/解锁,JIT 会把锁的范围扩大到整个操作序列之外,减少加解锁次数。
- 典型例子:循环体内反复
synchronized同一对象——与其加解锁 1000 次,不如把锁提到循环外加一次。
- 典型例子:循环体内反复
一句话区分:锁消除是"把不需要的锁去掉",锁粗化是"把零碎的锁合并成一把大的"。
14. 构造方法可以使用 synchronized 修饰吗?
答: 不能,也没有必要。
- 从语法上:
synchronized是方法修饰符,但 JVM 规范不允许synchronized修饰构造器,编译直接报错; - 从语义上:构造期间对象尚未对外发布(
this未逸出),不会被其他线程以引用方式访问,因此不存在"多个线程同时执行同一个构造方法"的场景,加锁没有意义。
关联陷阱:如果在构造器里把
this发布出去(启动线程、注册监听器),就制造了this逸出,此时对象确实会被并发访问——但问题出在"逸出",而不是"构造方法没加锁",正确做法是修正逸出而不是想办法给构造器加锁(见《Java并发(二)》安全发布一题)。
三、Lock 体系与 AQS
15. synchronized 与 ReentrantLock 的区别?
答:
| 维度 | synchronized | ReentrantLock |
|---|---|---|
| 实现层次 | JVM 关键字(Monitor) | JDK API(基于 AQS) |
| 释放方式 | 自动释放(异常也释放) | 必须手动 unlock(),通常放 finally |
| 可中断 | 不可中断 | 支持 lockInterruptibly() |
| 超时获取 | 不支持 | 支持 tryLock(timeout) |
| 公平性 | 只有非公平 | 可选公平 / 非公平 |
| 条件变量 | 一个等待集(wait/notify) | 可创建多个 Condition,精准唤醒 |
| 可重入 | 是 | 是 |
| 性能 | JDK 6 后优化充分,低竞争下更优 | 高竞争、需要高级功能时更优 |
共同点:都是可重入的互斥(悲观)锁,都保证可见性。
选择建议:优先用 synchronized(语法简单、自动释放、不会忘了解锁);仅在需要可中断、超时、公平锁或多条件变量时才用 ReentrantLock。
再次纠正:「
ReentrantLock是同步非阻塞、采用乐观并发策略」的说法是错误的。它获取不到锁时同样会阻塞等待,本质是悲观互斥锁,真正乐观的是 CAS。
16. 什么是 AQS?简述其原理。
答: AQS(AbstractQueuedSynchronizer)是 JUC 中锁与同步器的基础框架——ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock、ThreadPoolExecutor.Worker 都基于它实现。
两大核心组成:
- 一个
volatile int state:同步状态。语义由子类定义——在ReentrantLock中是重入次数,在Semaphore中是剩余许可数,在CountDownLatch中是剩余计数。 - 一个 FIFO 等待队列:是 CLH 队列的变体(Craig-Landin-Hagersten),双向链表,每个节点封装一个等待线程。
工作原理:
- 子类只需重写
tryAcquire/tryRelease(独占模式)或tryAcquireShared/tryReleaseShared(共享模式)来定义state的获取与释放语义,AQS 本身不关心"什么是锁"; - 获取失败时,用 CAS 把线程封装成节点加入队尾,并通过
LockSupport.park()挂起; - 持有者释放时修改
state,成功后唤醒后继节点,被唤醒线程重新尝试获取。
两种模式的差异:
| 模式 | 特点 | 典型实现 |
|---|---|---|
| 独占(EXCLUSIVE) | 只有一个线程能成功获取 | ReentrantLock |
| 共享(SHARED) | 多个线程可同时成功,且成功后继续唤醒后继节点(传播) | Semaphore、CountDownLatch |
公平与非公平的实现差异(ReentrantLock 为例):
- 公平锁:
tryAcquire先调用hasQueuedPredecessors(),检查队列中是否有比自己更早的等待者,有就放弃抢锁、老实排队; - 非公平锁:直接 CAS 抢
state,抢不到才入队。
17. ReentrantLock 的公平锁与非公平锁是如何实现的?默认哪种?
答: ReentrantLock 内部有 FairSync(公平) 与 NonfairSync(非公平) 两个内部类,都继承 AQS,差异只在 tryAcquire 的实现:
- 公平锁:先执行
hasQueuedPredecessors()判断队列中是否有更早的等待者,有则不抢,严格 FIFO; - 非公平锁:直接 CAS 抢
state,成功即占有,失败才入队。
默认是非公平锁:new ReentrantLock() 等价于 new ReentrantLock(false),传 true 才构造公平锁。
为什么默认非公平:非公平锁允许"刚释放锁的线程立刻再次抢到锁",省掉挂起与唤醒的线程切换开销,吞吐量明显更高;代价是可能造成饥饿,但在"锁持有时间短"的场景下饥饿并不突出。公平锁则每次都需要判断队列并排队,开销更大。
18. lock()、tryLock()、lockInterruptibly() 的区别?
答:
| 方法 | 是否阻塞 | 是否响应中断 | 是否可超时 |
|---|---|---|---|
lock() | 阻塞直到拿到锁 | 否(拿到锁后才响应,仅设置中断标志) | 否 |
lockInterruptibly() | 阻塞直到拿到锁 | 是,等待期间被中断抛 InterruptedException | 否 |
tryLock() | 不阻塞,立即返回 boolean | 否 | — |
tryLock(timeout, unit) | 最多等待 timeout | 是 | 是 |
两个实践要点:
tryLock(timeout)是规避死锁的利器:用统一的超时时间按序尝试获取所有锁,拿不到就释放已持有的锁并退避重试——这相当于破坏了死锁的"不可剥夺"条件;lock()不会因为interrupt()而中止等待,必须等拿到锁才响应中断,因此在对响应性敏感的场景(如虚拟线程)应优先用lockInterruptibly()。
19. Condition 与 Object 的 wait/notify 有什么区别?
答:
| 维度 | Object 监视器 | Condition |
|---|---|---|
| 获取方式 | 任意对象自带的隐式监视器 | lock.newCondition() 显式创建 |
| 等待队列数量 | 每个对象只有一个等待集 | 一把锁可创建多个 Condition |
| 等待方法 | wait() | await() |
| 唤醒方法 | notify() / notifyAll() | signal() / signalAll() |
| 唤醒精度 | notify() 随机唤醒,可能误唤醒无关线程 | 可按条件分组精准唤醒 |
| 使用前提 | 必须在 synchronized 内 | 必须持有对应的 Lock |
核心优势是"精准唤醒":在生产者-消费者这类存在多个等待条件的场景(队列满 / 队列空),用两个 Condition(notFull / notEmpty)分别管理——生产者只通知消费者、消费者只通知生产者,避免 notifyAll() 一次唤醒大量无关线程造成的无效竞争。ArrayBlockingQueue 正是这样实现的。
注意:
await()与wait()一样必须用while循环包裹以抵御虚假唤醒。
20. 什么是读写锁?ReentrantReadWriteLock 支持锁升级吗?
答: ReentrantReadWriteLock 内部维护一对锁:ReadLock(共享锁)与 WriteLock(独占锁),共用同一个 AQS,通过 state 的高低 16 位分别记录读锁与写锁的持有次数。
并发规则:
| 组合 | 是否允许并发 |
|---|---|
| 读 - 读 | ✅ 允许(共享) |
| 读 - 写 | ❌ 互斥 |
| 写 - 写 | ❌ 互斥 |
适合读多写少的场景;缺点是可能造成写线程饥饿(读锁源源不断,写线程一直拿不到),因此它支持可选的公平模式。
锁升级 vs 锁降级(高频考点):
- 锁降级:支持——持有写锁的线程可以再获取读锁,然后释放写锁,即"写锁 → 读锁"。
rwLock.writeLock().lock();
try {
// 1. 修改数据
rwLock.readLock().lock(); // 2. 降级:写锁未释放时先拿读锁
} finally {
rwLock.writeLock().unlock(); // 3. 释放写锁,此时仍持有读锁
}
// 4. 以读锁身份继续读,保证读取期间数据不被其他写线程修改- 锁升级:不支持——持有读锁时不能直接获取写锁。因为可能有多个线程同时持有读锁,若它们都去等写锁,就会互相等待形成死锁。
为什么需要锁降级:在"写完立刻要读,且希望读取期间数据不被其他写线程改动"的场景,降级能在不释放锁的前提下同时获得写入可见性与读取原子性。
21. StampedLock 是什么?它的乐观读是如何实现的?
答: StampedLock 是 JDK 8 引入的锁,性能优于 ReentrantReadWriteLock,支持三种模式:
| 模式 | 方法 | 说明 |
|---|---|---|
| 写锁 | writeLock() | 独占,可能阻塞 |
| 悲观读锁 | readLock() | 共享,可能阻塞 |
| 乐观读 | tryOptimisticRead() | 不加锁,仅返回一个版本戳 stamp |
乐观读的实现:tryOptimisticRead() 不阻塞也不加锁,只返回一个版本戳(stamp,返回 0 表示当前有写锁占用、获取失败);读取数据后调用 validate(stamp) 校验"读取期间是否发生过写操作":
long stamp = lock.tryOptimisticRead(); // 1. 乐观读,不加锁
int curX = x, curY = y; // 2. 把数据读到局部变量
if (!lock.validate(stamp)) { // 3. 校验:期间被写过?
stamp = lock.readLock(); // 4. 校验失败则升级为悲观读锁重读
try {
curX = x; curY = y;
} finally {
lock.unlockRead(stamp);
}
}三大局限(务必记住):
- 不可重入——同一线程重复获取会死锁;
- 不支持
Condition; - 不保证公平,且暴露的是 stamp 而非锁对象,必须严格配对释放(
unlockWrite(stamp)/unlockRead(stamp)),不能跨方法传递。
适用场景:读极多、写极少,且希望读操作完全没有加锁开销(如只读快照、坐标/配置读取)。
