锁优化与无锁编程

共 17 题
📑 题目列表 17 题
#
★★★

1. JDK 25 中 AtomicReferenceArray 的细粒度 CAS 在无锁队列中的实现

JDK 25 中 AtomicReferenceArray 的细粒度 CAS 在无锁队列中如何实现?

  • AtomicReferenceArray
  • 细粒度 CAS
  • 无锁队列

无锁环形队列(如 Disruptor 风格的 SPSC 队列)用 AtomicReferenceArray 存槽位,每个槽位是独立元素,通过细粒度 CAS 更新槽位(如写入/消费时 CAS 槽位的引用或状态),实现无锁的入队出队。JDK 25 中 AtomicReferenceArray 用 VarHandle 对每个元素做 CAS,元素级原子,避免整体锁。细粒度 CAS 让多个槽位可并发操作,提高吞吐。实现要点:用索引模数组长度定位槽位,CAS 更新槽位状态避免 ABA(配合版本号)。

AtomicReferenceArray 元素级 CAS 是环形无锁队列的基础,配合索引与版本 CAS 实现无锁并发。

#
★★★

2. 无锁栈(Lock-Free Stack)的实现

无锁栈(Lock-Free Stack)如何实现?

  • 无锁栈
  • 头指针 CAS
  • ABA 处理

无锁栈用 AtomicReference 或 AtomicReferenceArray 维护头指针(top),push 时构造新节点并 CAS 把头指针指向新节点(新节点.next=旧头),pop 时 CAS 把 head 更新为 head.next。CAS 失败则重试(自旋)。关键问题:ABA(pop 过程中头被其他线程修改又恢复),需用 AtomicStampedReference(版本戳)或不可复用节点解决。push/pop 都是 lock-free:至少一个线程取得进展。是无锁数据结构的基础示例。

无锁栈 = 头指针 CAS + 重试 + ABA 处理。是理解无锁编程的入门示例。

AtomicReference<Node> head = new AtomicReference<>();
void push(Node n) { Node old; do { old = head.get(); n.next = old; } while (!head.compareAndSet(old, n)); }
Node pop() { Node old, next; do { old = head.get(); if (old == null) return null; next = old.next; } while (!head.compareAndSet(old, next)); return old; }
#
★★★

3. 无锁算法在 JCTools(SpscArrayQueue)的应用

无锁算法在 JCTools(SpscArrayQueue)的应用是什么?

  • JCTools
  • SpscArrayQueue
  • 无锁单生产者单消费者

JCTools 是高性能并发集合库,SpscArrayQueue 是"单生产者-单消费者"无锁环形队列。核心:利用 SPSC 约束(一个生产者、一个消费者),消除竞争,用 volatile 索引 + 缓存行填充(避免伪共享)+ 无锁 CAS(或仅 volatile 读写)实现极高吞吐。生产者只写 tail 索引,消费者只读 head 索引,各自持有独立索引,无锁。JCTools 的 SpscArrayQueue 在 Disruptor 类似的场景中,比 JDK 内置队列吞吐更高。应用:高性能事件传递、日志、网络缓冲。

SpscArrayQueue 利用 SPSC 约束消除竞争,配合缓存行填充与 volatile 索引,是无锁高吞吐的代表。

#
★★★

4. 无锁算法在内存可见性保证上的底层 VarHandle.acquireFence/releaseFence 依赖

无锁算法在内存可见性保证上如何依赖 VarHandle.acquireFence/releaseFence?

  • acquireFence/releaseFence
  • 无锁算法可见性
  • 发布/获取

无锁算法常在只依赖原子性(plain CAS)的槽位更新后,需要保证数据可见性。releaseFence 在写入数据后、发布索引前插入,确保"数据写入先于索引发布";acquireFence 在读取索引后、消费数据前插入,确保"索引读取后能读到已发布的数据"。二者配对建立"发布-获取"关系,保证消费者读到生产者发布的完整数据,无需 volatile 全序。JCTools 等无锁队列用 VarHandle 的 plain/acquire/release 与 fence 优化可见性,降低屏障成本。

release/acquire fence 是"发布-获取"的廉价可见性手段,无锁算法用它避免每次 volatile 全序的开销。

#
★★

5. 无锁编程的可线性化(Linearizability)定义

无锁编程的可线性化(Linearizability)定义是什么?

  • 可线性化
  • 线性化点
  • 并发正确性

可线性化是并发操作的正确性标准:每个操作在并发执行中有一个"线性化点"(操作真正生效的时刻),所有操作可被看作按线性化点的顺序串行执行,且该顺序与真实时间顺序一致(每个操作在调用与返回之间原子生效)。无锁算法必须保证可线性化:每个操作(如 push/pop)的 CAS 是线性化点,读者能观察到一致的操作顺序。可线性化是"强一致"的并发语义,强于顺序一致(额外要求每个操作的线性化点位于调用与返回之间,满足实时约束)。

可线性化 = 每个操作原子生效(线性化点),整体可串行解释。无锁算法正确性以此为标准。

#
★★

6. 无锁队列(Lock-Free Queue)的算法基础

无锁队列(Lock-Free Queue)的算法基础是什么?

  • 无锁队列
  • 头尾 CAS
  • 算法

无锁队列算法基础:双向链表(如 Michael & Scott)或环形数组。链表式:head 出队、tail 入队,用 CAS 更新 head/tail;入队时 CAS 把新节点追加到 tail 后,出队时 CAS 摘除 head。环形数组式:用索引+槽位,CAS 更新写/读索引。关键:1) 无锁(至少一个线程进展);2) 处理 ABA(版本戳);3) 处理伪共享(头尾分离);4) 用 volatile/acquire-release 保证可见性。M&S 队列是经典,JDK 的 ConcurrentLinkedQueue 基于它。

无锁队列基础 = 头尾 CAS + ABA 处理 + 可见性维护。链表或环形数组两种实现。

#
★★

7. CAS 竞争下的缓存一致性代价(cache line ping-pong)与伪共享缓解

CAS 竞争下的缓存一致性代价(cache line ping-pong)与伪共享缓解是什么?

  • cache line ping-pong
  • 伪共享
  • 缓解

高并发 CAS 竞争时,多个线程争抢同一缓存行,每次 CAS 成功使其他线程的缓存副本失效,其他线程重新读取,形成"缓存行 ping-pong"(在核间来回传递),总线流量与缓存 miss 大增,吞吐下降。缓解:1) 缓存行填充(@Contended)隔离热字段,避免伪共享;2) 分段/分散(如 LongAdder 的 Cell[])把写分散到不同缓存行;3) 减少共享热变量;4) 退避降低争用。核心是"让热变量各自独占缓存行,减少 ping-pong"。

cache line ping-pong 是 CAS 高竞争的硬件代价。缓解靠缓存行填充与分段分散。

#
★★

8. Hazard Pointer 与 RCU 的对比

Hazard Pointer 与 RCU 的对比是什么?

  • Hazard Pointer
  • RCU
  • 对比

Hazard Pointer(危险指针):无锁数据结构中保护被读取的节点不被回收,读者声明"正引用某节点",写者回收时检查是否有读者声明,避免访问已释放节点。RCU(Read-Copy-Update):读不用锁,写时复制副本再原子替换,旧副本延迟到所有读者退出后回收。对比:两者都解决"回收与并发访问"问题;Hazard Pointer 细粒度(节点级),RCU 粗粒度(整体替换);RCU 写开销大但读无锁,Hazard Pointer 需维护危险指针列表。Java 中 GC 常替代两者(GC 自动回收),但无锁结构仍需逻辑处理。

两者都是"安全回收"机制,Hazard Pointer 节点级、RCU 整体替换。Java GC 缓解了内存回收需求。

#
★★

9. RCU(Read-Copy-Update)思想在 Java 中的模拟

RCU(Read-Copy-Update)思想在 Java 中如何模拟?

  • RCU 思想
  • Java 模拟
  • Copy-on-Write

RCU 思想:读无锁、写时复制、延迟回收旧副本。Java 中模拟:1) CopyOnWriteArrayList/ConcurrentHashMap 的"写时复制"(Copy-on-Write)是 RCU 的变体;2) 用 AtomicReference 持有不可变快照,写时构造新对象 CAS 替换,读者读旧快照无锁;3) 旧副本由 GC 自动回收(替代 RCU 的主动等待)。Java 中 RCU 的"延迟回收"由 GC 完成,开发者只需用"不可变快照 + 原子替换 + 读无锁"模式模拟 RCU 的读优写少的特性。

Java 模拟 RCU = Copy-on-Write + 原子替换 + GC 回收。读无锁、写复制、GC 释放旧副本。

#
★★

10. 锁粗化与锁消除,JIT 在什么条件下可以消除同步,与逃逸分析的关系如何?

锁粗化与锁消除:JIT 在什么条件下可以消除同步?与逃逸分析的关系是什么?

  • 锁消除
  • 锁粗化
  • 逃逸分析

锁消除:JIT 通过逃逸分析发现锁对象不逃逸(仅当前线程使用,不发布到其他线程),则该同步无跨线程意义,删除加锁。条件是有逃逸分析证明对象不逃逸。锁粗化:JIT 发现相邻的多次加锁/解锁(如循环内反复 synchronized 同一对象)合并为一次。二者关系:锁消除依赖逃逸分析(证明无竞争),锁粗化不依赖逃逸(合并相邻同步)。只有对象不逃逸时才能锁消除,逃逸对象不能消除。JIT 对 synchronized 优化,ReentrantLock 不可自动消除。

锁消除靠逃逸分析证明"无并发",锁粗化合并相邻临界区。逃逸分析是锁消除的前提。

#
★★

11. 自适应自旋(Adaptive Spinning),JVM 如何根据竞争历史调整自旋次数?

自适应自旋(Adaptive Spinning):JVM 如何根据竞争历史调整自旋次数?

  • 自适应自旋
  • 竞争历史
  • 调整

自适应自旋(JDK 6+)让 JVM 根据"上次锁的自旋成功与否"与"当前持锁时间"动态调整自旋次数:若上次自旋成功(在自旋期间获锁),则本次增加自旋次数;若上次自旋失败(自旋超时未获锁),则减少自旋次数。同时根据持锁线程的历史持锁时间判断"自旋是否划算"。通过维护每个锁的自旋状态,JVM 在"自旋等待"与"阻塞"间自适应,兼顾低竞争的低延迟与高竞争的 CPU 节省。这是 synchronized 锁升级(轻量→重量)中的决策。

自适应自旋用"历史成功率"调自旋次数,是 JVM 对竞争度的智能响应。

#
★★

12. 无锁与加锁在高竞争/低竞争场景的性能拐点与选择依据

无锁与加锁在高竞争/低竞争场景的性能拐点与选择依据是什么?

  • 低竞争无锁优势
  • 高竞争拐点
  • 选择依据

低竞争时无锁(CAS 自旋)几乎无竞争,一次成功,吞吐高、无阻塞;加锁有 park/unpark 与切换开销。高竞争时无锁 CAS 大量线程空转、缓存行 ping-pong,吞吐下降甚至低于加锁;加锁让线程阻塞、释放 CPU,吞吐更稳定。存在"性能拐点":竞争度超过某阈值后加锁反超无锁。选择依据:1) 竞争度(压测);2) 临界区长度(短用无锁,长用加锁);3) 延迟要求(无锁低延迟,加锁尾延迟稳定);4) 实现复杂度。无锁适合低竞争短临界区,加锁适合高竞争长临界区。

无锁 vs 加锁是"忙等 vs 阻塞"的拐点问题。低竞争短临界区无锁,高竞争长临界区加锁。

#
★★

13. VarHandle 与 AtomicXxx 在能力与内存序控制上的差异

VarHandle 与 AtomicXxx 在能力与内存序控制上的差异是什么?

  • VarHandle 能力
  • AtomicXxx 能力
  • 内存序差异

VarHandle 提供更细的能力:可对任意字段/数组元素做原子操作(不限于特定类),并支持完整内存序控制(plain/opaque/acquire/release/volatile + weak/acquire/release 变体 + fence)。AtomicXxx 是特定类型封装(AtomicInteger/AtomicReference 等),只提供 volatile 语义的原子操作,无法自定义内存序,也不能操作任意字段。差异:VarHandle 更灵活(任意目标+内存序),AtomicXxx 更简单(类型安全、开箱即用)。底层 Atomic 都基于 VarHandle 实现。需要降内存序或操作任意字段用 VarHandle,否则用 AtomicXxx。

VarHandle 是"通用原子+内存序"引擎,AtomicXxx 是"便捷封装"。能力与内存序控制 VarHandle 更强。

#
★★

14. 无锁编程的 ABA 问题,AtomicStampedReference 如何解决,与乐观锁如何类比?

无锁编程的 ABA 问题:AtomicStampedReference 如何解决?与乐观锁的类比是什么?

  • ABA 问题
  • AtomicStampedReference
  • 乐观锁类比

ABA 问题:CAS 比较值相等但状态已变回,误判未变。AtomicStampedReference 用"引用+版本戳"解决:CAS 同时比较引用与版本戳,每次修改版本戳递增,即使值回绕到相同,版本戳不同,CAS 失败。乐观锁类比:数据库乐观锁用"版本号/时间戳"字段,更新时 WHERE version=期望值,版本变化则更新失败——与 AtomicStampedReference 的版本戳思想一致。两者都是"用版本号检测并发修改"。

AtomicStampedReference 与数据库乐观锁都用"版本号检测修改",机制同源,只是内存 vs 数据库。

#
★★

15. 偏向锁/轻量级锁/重量级锁的升级,JVM 锁膨胀的触发条件与性能影响如何?

偏向锁/轻量级锁/重量级锁的升级:JVM 锁膨胀的触发条件与性能影响是什么?

  • 锁升级路径
  • 触发条件
  • 性能影响

synchronized 锁升级:无锁→偏向锁→轻量级锁→重量级锁。触发条件:1) 偏向锁:首次获取记录线程 ID,后续该线程直接进入;其他线程竞争时撤销偏向(JDK 15 默认禁用);2) 轻量级锁:无竞争或低竞争时 CAS 修改 Mark Word 指向栈锁记录,自旋;3) 重量级锁:自旋失败或竞争激烈,升级为 OS Monitor,线程阻塞。性能影响:升级增加开销(从 CAS 到阻塞),但按需选择最轻机制;重量级锁有上下文切换,偏向锁有撤销成本。锁膨胀是"竞争加剧时渐次加大开销"。

锁升级是"竞争度响应":低竞争用轻量 CAS,高竞争升级重量级阻塞。JIT 自适应自旋影响升级时机。

#
★★

16. volatile 的语义,可见性/有序性与原子性的边界如何?

volatile 的语义:可见性/有序性与原子性的边界是什么?

  • 可见性
  • 有序性
  • 原子性边界

volatile 保证可见性(写对其他线程可见)与有序性(禁止重排,建立屏障),但不保证原子性:volatile 的读-改-写(如 i++)不是原子的,多线程并发生会丢更新。边界:volatile 适合"单次读写"(状态标志、发布不可变引用),不适合"复合操作"(计数、检查-修改)。复合操作需原子类(AtomicInteger)或锁。可见性/有序性由 volatile 保证,原子性由原子类/锁保证,二者互补。

volatile 的边界是"无原子性"。复合操作必须用原子类或锁,volatile 只做单次读写。

#

17. Disruptor 的缓存行填充与无锁环形缓冲(Sequence)设计

Disruptor 的缓存行填充与无锁环形缓冲(Sequence)设计是什么?

  • Disruptor
  • 缓存行填充
  • 无锁环形缓冲

Disruptor 是高性能无锁环形缓冲(LMAX 框架)。设计:1) Pre-allocated 环形数组(预分配事件,避免 GC);2) Sequence(序号)用缓存行填充(@Contended),避免伪共享,每个消费者/生产者独立 Sequence;3) 无锁:用 CAS 更新 Sequence 与槽位,配合屏障(SequenceBarrier)协调消费;4) 单生产者单消费者可达极高吞吐。缓存行填充让每个 Sequence 独占缓存行,无锁 CAS 消除锁竞争。Disruptor 用于超低延迟事件传递(如交易系统)。

Disruptor 用"预分配 + 缓存行填充 + 无锁 CAS Sequence"实现超高吞吐,是缓存行填充与无锁的典范。