CAS 与原子类

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

1. AtomicInteger 在高并发计数中的瓶颈

AtomicInteger 在高并发计数时会遇到什么瓶颈?

  • CAS 循环的竞争
  • 缓存行争用(cache line ping-pong)
  • 与其他方案对比

高并发下多个线程同时 getAndIncrement 时,CAS 竞争激烈,大量线程 CAS 失败后重试,形成自旋。更深层的瓶颈是缓存一致性:所有线程争抢同一个缓存行(state 变量所在),每次 CAS 成功失效其他线程的缓存副本,造成 cache line ping-pong 和总线流量上升,吞吐受限。写多读多的热点计数场景下,AtomicInteger 的吞吐会被单点 CAS 竞争限制,这时应改用 LongAdder(分段计数)或 Striped64 机制分散热点。

瓶颈本质是"单点原子变量 + 缓存行竞争"。AtomicInteger 的 CAS 每次都是全局原子操作,并发度高时成为吞吐瓶颈,LongAdder 通过 cell 分段缓解。

#
★★★

2. AtomicInteger 的 getAndIncrement 在高并发下的伪共享(False Sharing)问题与 @Contended

AtomicInteger 的 getAndIncrement 在高并发下如何处理伪共享(False Sharing)问题?@Contended 注解的作用是什么?

  • 伪共享的原理
  • 缓存行填充(padding)
  • @Contended 注解与 JVM 参数

伪共享指多个原子变量恰好落在同一缓存行(64 字节)内,当一个线程更新其中一个时,会使整个缓存行失效,导致其他线程更新相邻变量时缓存不命中,互相拖慢。@Contended 注解(JDK 8)让 JVM 在字段前后填充缓存行,使每个字段独占缓存行,避免伪共享。使用时需加 -XX:-RestrictContended(或 -XX:+RestrictContended 谨慎)允许用户使用。AtomicLong 内部并不用 @Contended,但 LongAdder 的 Cell 数组用 @Contended 防止 cell 间伪共享。

伪共享是并发性能的隐性杀手。@Contended 通过缓存行填充隔离热字段,但会增大内存占用,适用于少量热点字段。

#
★★★

3. AtomicInteger 的 weakCompareAndSet 与 compareAndSet 在失败语义、内存可见性和 CAS 循环写法上有哪些区别

AtomicInteger 的 weakCompareAndSet 与 compareAndSet 在失败语义、内存可见性和 CAS 循环写法上有哪些区别?

  • 失败语义(weak 允许伪失败)
  • 内存可见性(weak 无 volatile 全排)
  • CAS 循环写法差异

compareAndSet 保证强语义:成功时写入对所有线程可见(volatile 全序),失败时返回 false 且确定性;weakCompareAndSet 是弱语义:允许伪失败(即使条件满足也可能返回 false),且不保证与 volatile 相同的强内存序(只保证原子性,不保证全序),适合在循环中反复重试。写法上,weak 版本必须放在循环里(因为允许伪失败),而 compareAndSet 通常也可循环但语义更强。注意 JDK 9 起 weakCompareAndSet 被标记为 volatile 语义(与 compareAndSet 等同),真正的弱版本是 VarHandle.weakCompareAndSetPlain。

语义差异的根源是内存序:compareAndSet 是 volatile 读-写,weak 是 plain 或不保证全序。JDK 9 后 weak 方法名与行为有变化,实际弱 CAS 用 VarHandle 的 weak 模式。

#
★★★

4. AtomicInteger/AtomicReference 的 CAS 与内存序

AtomicInteger/AtomicReference 的 CAS 操作具有什么内存序?对可见性有何保证?

  • CAS 的 volatile 读-写语义
  • 内存序(acquire/release 全序)
  • 与 plain 读写的区别

AtomicInteger/AtomicReference 的 compareAndSet 具有 volatile 读-写语义,即具备完整的内存序:成功的 CAS 对其他线程的可见性服从全序(total order),配合 volatile 读写建立 happens-before。CAS 成功相当于一次 volatile 写,读取(get)相当于 volatile 读。因此 CAS 循环能保证更新后的值对其他线程可见。内存序更强于 plain 或 opaque,但也会带来更多同步开销。若只需原子性不需强可见性,可用 VarHandle 的 weak/opaque 模式降低开销。

CAS 的"读-改-写"是原子的且带 volatile 语义,这是无锁数据结构可见性正确的基础。理解"CAS 成功 = volatile 写"是关键。

#
★★★

5. AtomicIntegerArray 与 AtomicLongArray 的数组原子性

AtomicIntegerArray 与 AtomicLongArray 的数组原子性是如何保证的?

  • 数组元素索引的原子更新
  • 内部实现(Unsafe 数组基址 + 偏移)
  • 与普通数组的区别

AtomicIntegerArray 对每个数组元素提供原子操作(getAndIncrement、compareAndSet 等)。它对数组元素本身实现原子性,通过 Unsafe/VarHandle 以"数组基址 + 元素偏移"定位元素,对单个元素做 CAS。它只保证单元素操作的原子性,不保证多元素组合操作(如交换两个元素)的原子性。普通数组的元素读写非原子且无 volatile 元素级语义,AtomicIntegerArray 弥补了这一点。

数组原子性 = 每个元素可独立 CAS。注意它原子的是"单个元素",多元素不变量需额外同步或版本化。

#
★★★

6. AtomicIntegerArray 只保证单个元素操作原子,多元素不变量需要采用什么同步或版本化方案

AtomicIntegerArray 只保证单个元素操作原子,那么多元素不变量需要采用什么同步或版本化方案?

  • 多元素不变量的问题
  • 同步方案(锁)
  • 版本化方案(CAS 整个数组或拷贝)

当需要保证多个元素组成的整体不变量(如"A+B 之和恒定")原子时,靠单元素 CAS 不够。方案:1) 用锁(synchronized/ReentrantLock)包裹整个多元素操作,保证原子性;2) 用不可变快照+版本号:把整个数组封装成不可变对象,用 AtomicReference 对一个含数组的不可变引用做 CAS,一次替换保证多元素原子;3) 用版本号/序号校验,读取后校验版本确保一致性。核心是"要么加锁、要么整体 CAS 一个不可变对象"。

单元素原子性无法覆盖多元素组合,必须把"组合"提升为单个原子操作(锁或整对象 CAS)。版本化适合"先读后验"的乐观场景。

#
★★★

7. AtomicIntegerFieldUpdater 对目标字段的 volatile、类型和访问权限有哪些强制要求

AtomicIntegerFieldUpdater 对目标字段的 volatile、类型和访问权限有哪些强制要求?

  • 字段必须 volatile
  • 类型匹配(int/Integer)
  • 访问权限与实例定位

AtomicIntegerFieldUpdater 要求目标字段:1) 必须是 volatile(否则构造时抛 IllegalArgumentException);2) 类型必须匹配(int 字段对应 AtomicIntegerFieldUpdater,long、引用分别对应各自;用包装类型 Integer 时需注意);3) 字段必须可被访问(通常为 public 或通过 setAccessible,且需与创建 updater 的类位于同一包或可访问);4) 通过实例对象更新的字段必须是实例字段(非 static)。访问权限不满足时会抛异常或无法构造。

FieldUpdater 用反射+Unsafe 更新字段,省去 AtomicInteger 对象分配,但字段必须 volatile 且类型/权限匹配,否则无法工作。

#
★★★

8. AtomicReference 与 volatile 引用在发布不可变对象时的取舍

AtomicReference 与 volatile 引用在发布不可变对象时的取舍是什么?

  • 两者可见性差异
  • 原子读-改-写能力
  • 发布不可变对象的场景

volatile 引用只保证引用的可见性(读到的引用是最新的),但不能原子地"读-改-写"(如基于当前值判断后更新)。AtomicReference 在 volatile 可见性基础上提供原子 compareAndSet/getAndUpdate 等读-改-写操作,适合"看到旧值后根据条件更新、需要原子性"的场景。发布不可变对象(如配置快照)时,若只需"读到最新引用",volatile 足够且更简单;若需要"CAS 更新引用"(如自旋更新、一次性设置),用 AtomicReference。

取舍核心是"是否需要在可见性之外做原子读-改-写"。volatile 只发布,AtomicReference 发布+原子更新。

#
★★★

9. AtomicReference 与对象引用的原子更新

AtomicReference 如何实现对象引用的原子更新?

  • 引用类型的 CAS
  • compareAndSet 的用法
  • 与不可变对象配合

AtomicReference 包装一个 volatile 引用,compareAndSet(expected, update) 在"当前引用与 expected 相等"时原子地替换为 update,返回是否成功。可用于:无锁栈/队列的指针推进、一次性初始化(CAS null→实例)、不可变状态的整体替换(一次 CAS 更新多个字段)。更新常配合不可变对象:构造新对象后 CAS 替换引用,读者看到的是完整新状态。也可用 getAndUpdate/updateAndGet 提供函数式原子更新。

引用原子更新的核心是"对引用本身做 CAS"。配合不可变对象可实现"多字段不变量一次的原子切换"。

AtomicReference<Config> ref = new AtomicReference<>(oldConfig);
Config next = ref.get().with(featureFlag);
ref.compareAndSet(oldConfig, next); // 原子替换整份配置
#
★★★

10. CAS 与 Lock 在临界区大小不同场景下的吞吐取舍

CAS 与 Lock 在临界区大小不同的场景下吞吐有何取舍?

  • 临界区小(短)时 CAS 优势
  • 临界区大(长)时 Lock 优势
  • 竞争度的影响

临界区极短(如计数器自增、几十条指令)时,CAS 自旋开销低、无上下文切换,吞吐高;临界区较长(涉及复杂计算或 IO)时,CAS 自旋会长时间空转 CPU 且其他线程持续失败重试,此时 Lock 让线程 park 阻塞更高效。竞争度也关键:低竞争且短临界区用 CAS,高竞争用 Lock。经验上,临界区若在自旋等待时间内大概率完成,CAS 划算;否则 Lock 更优。

"自旋 vs 阻塞"取决于临界区长度与竞争度。短临界区 CAS 胜,长临界区/高竞争 Lock 胜。这是并发设计经典权衡。

#
★★★

11. CAS 与锁的吞吐量对比(高竞争/低竞争)

在高竞争与低竞争场景下,CAS 与锁的吞吐量如何对比?

  • 低竞争时 CAS 优势
  • 高竞争时锁与 CAS 的对比
  • 吞吐拐点

低竞争(并发线程少、争用低)时,CAS 几乎无竞争,一次成功,吞吐高;锁则有 park/unpark 与上下文切换开销,吞吐略低。高竞争时,CAS 大量线程同时空转、缓存行 ping-pong,无效重试浪费 CPU,吞吐不升反降;锁通过阻塞让线程让出 CPU,配合 OS 调度,吞吐更稳定且可扩展。存在一个"吞吐拐点":竞争度超过某阈值后,锁的吞吐反超 CAS。因此无锁结构适合低竞争,高竞争需谨慎并考虑退避。

高竞争下 CAS 的"繁忙等待"反而成为瓶颈,锁的阻塞调度更优。选型需结合实际竞争度做压测。

#
★★★

12. CAS 失败后丢弃预先分配的新对象会造成什么分配压力,怎样减少热点路径上的无效对象

CAS 失败后丢弃预先分配的新对象会造成什么分配压力?如何减少热点路径上的无效对象?

  • CAS 失败时丢弃对象的分配垃圾
  • 对象分配与 GC 压力
  • 减少无效分配的策略

在 CAS 循环中,若先构造新对象再 CAS 更新引用,CAS 失败时该对象被丢弃,形成分配垃圾。高竞争下大量失败导致大量无效对象分配,增加 GC 压力与分配停顿。减少策略:1) CAS 失败时直接重试、不重新构造(复用失败前的旧对象,仅当确认成功才构造新对象);2) 用可变/惰性构造,减少每次失败的对象创建;3) 用对象池或复用;4) 用分段结构(如 LongAdder)避免热点冲突。核心是"只在 CAS 成功前一次性构造,或失败时避免重复分配"。

无效对象分配是 CAS 循环的隐性成本。优化思路是"延迟构造、失败不重造",把分配次数降到最低。

#
★★★

13. CAS 循环在调度不公平时可能让单个线程长期饥饿,系统应通过哪些指标识别这种现象

CAS 循环在调度不公平时可能让单个线程长期饥饿,系统应通过哪些指标识别这种现象?

  • CAS 循环的饥饿
  • 相关指标(CPU 占用、重试次数、延迟分布)
  • 识别手段

非公平的 CAS 循环中,某些线程可能持续 CAS 失败而长期拿不到更新权,形成饥饿。识别指标:1) 线程 CPU 使用率异常高但吞吐低(大量空转重试);2) CAS 重试次数/失败率升高(可通过计数器或 JFR 采样);3) 延迟分布 P99/P999 恶化,个别线程响应极慢;4) 线程 dump 中大量线程处于 RUNNABLE 且不断自旋(无阻塞)。可用 JFR 的线程采样、JMX 或 profiling 定位哪些线程在自旋,结合重试率判断是否饥饿。

饥饿表象是"某线程迟迟不获进展"。通过 CPU 空转、重试率、延迟长尾与线程栈自旋特征综合识别。

#
★★★

14. CAS 操作在长自旋(Spin)下的 CPU 空转问题与 Backoff 策略

CAS 操作在长自旋下会造成什么 CPU 空转问题?Backoff 策略如何解决?

  • 长自旋的 CPU 空转
  • 退避(backoff)策略
  • 与活锁/公平的关系

高竞争下 CAS 长时间自旋,线程持续空转轮询,占用 CPU 但不产生进展,浪费处理器资源。Backoff 策略:自旋失败后暂停一段时间(固定或指数递增)再重试,降低对同一缓存行/总线争用,减少无效失败,提高整体吞吐。但退避要避免"同步活锁":多个线程退避节奏相同可能同时重试又同时失败,应加随机抖动(random jitter)错开。Backoff 是降低竞争、提升可扩展性的常用手段。

长自旋的本质是"用 CPU 换延迟"。Backoff+随机抖动在竞争激烈时减少 ping-pong,牺牲单线程延迟换取整体吞吐。

#
★★★

15. CAS 自旋的开销与退避策略

CAS 自旋的开销是什么?如何设计退避策略?

  • 自旋开销(CPU、总线、缓存)
  • 指数退避
  • 随机抖动

CAS 自旋开销:CPU 空转、缓存行争用(ping-pong)、总线流量增大、cache miss 增多。退避策略设计:1) 固定短退避(如 Thread.onSpinWait 提示);2) 指数退避(起退避 + 翻倍,如 1,2,4,8... 到上限);3) 随机抖动(在指数范围内加随机,避免同步活锁);4) 设重试上限,超过则放弃或转阻塞。根据竞争度调整退避参数,平衡延迟与吞吐。

退避是把"激烈竞争"转为"有序重试"。指数+随机是最佳组合,既快速规避短期竞争又避免周期同步。

#
★★

16. CAS(Compare-And-Swap)的硬件实现与 ABA 问题

CAS(Compare-And-Swap)的硬件实现是什么?ABA 问题是什么?

  • 硬件 CAS(LOCK CMPXCHG)
  • 原子性保证
  • ABA 问题与解决

硬件 CAS 通过 CPU 的原子指令(x86 的 LOCK CMPXCHG、ARM 的 LDXR/STXR)实现"比较-交换"的原子性,LOCK 前缀锁定总线/缓存行保证指令不可分割。ABA 问题:CAS 只看"当前值是否等于期望值",若值从 A 变成 B 再变回 A,CAS 会误认为未被修改。无锁栈用头指针比较时,若另一个线程弹出再压回相同节点,头指针恢复原值,CAS 误判成功导致错误。解决:AtomicStampedReference(版本号)、AtomicMarkableReference(布尔标记)、不可复用节点。

ABA 是 CAS 的经典陷阱,根源是"值相等不等于状态未变"。用版本戳或逻辑标记区分"同一值但不同代"。

#
★★

17. JDK 9 VarHandle 的原子访问模式

JDK 9 引入的 VarHandle 提供了哪些原子访问模式?

  • VarHandle 的定位
  • 原子访问模式(compareAndSet、getAndSet 等)
  • 与 Unsafe/Atomic 的关系

VarHandle 是 JDK 9 引入的替代 Unsafe 的官方 API,对任意字段/数组元素提供原子访问操作:compareAndSet、compareAndExchange(返回 witness 值)、getAndSet、getAndAdd、getAndUpdate、getAndIncrement 等,以及内存序控制的 getAcquire/setRelease/opaque/plain 访问。相比 AtomicXxx,VarHandle 更灵活,可对任意类字段做原子操作,并可选择内存序(volatile/acquire/release/opaque/plain)。它是 JDK 未来原子操作的标准接入点。

VarHandle 统一了"原子+内存序"控制,是 Unsafe 的官方替代。Atomic 类底层也迁移到 VarHandle 实现。

#
★★

18. Java 9 VarHandle 的 release/acquire 模式与 volatile 演进

Java 9 VarHandle 的 release/acquire 访问模式与 volatile 有何演进关系?

  • release/acquire 语义
  • 与 volatile 全序的区别
  • 何时可用 release/acquire

release/acquire 是弱于 volatile 的内存序:release 写保证"写之前的所有操作不被重排到写之后",acquire 读保证"读之后的操作不被重排到读之前",二者配对建立 happens-before,但不提供 volatile 的全序(total order)与完全禁止重排。volatile 是 strongest(读-写全序)。release/acquire 允许更多重排,因此开销更低,适合"生产者发布、消费者获取"的发布-获取模式,无需全序语义时可用,降低同步成本。

volatile 是全序并发,release/acquire 是"前向/后向"单向约束。在明确"发布-消费"关系的无锁算法中,用 release/acquire 更省资源。

#
★★

19. Java 垃圾回收降低了无锁结构的内存回收难度,但为何仍不能自动消除逻辑 ABA

Java GC 降低了无锁结构的内存回收难度,但为何仍不能自动消除逻辑 ABA?

  • GC 对内存回收的帮助
  • 逻辑 ABA 的本质(值相等但状态已变)
  • 为什么 GC 无法解决

Java GC 自动回收无锁结构中的废弃节点,避免了手动内存回收(像 Hazard Pointer 那样)的复杂度,降低了"内存回收型 ABA"(节点被复用导致的问题)。但 GC 无法消除逻辑 ABA:逻辑 ABA 是"值从 A 变到 B 再变回 A,CAS 无法区分两代"问题,与内存是否回收无关。即使节点不被复用,只要业务逻辑上值"回绕"到相同,CAS 仍会误判。因此必须用版本戳(AtomicStampedReference)或逻辑标记显式区分。

GC 解决"物理复用",版本戳解决"逻辑回绕"。两者层次不同:GC 管内存,逻辑 ABA 需要开发者用版本/标记解决。

#
★★

20. Long/Double 的非原子写入与 JMM 处理

Long/Double 的非原子写入问题是什么?JMM 如何处理?

  • 64 位写入的非原子性
  • JMM 对 volatile long/double 的保证
  • 非 volatile 的 64 位写入

在 32 位 JVM 上,long/double 占 64 位,写入可能被拆成两次 32 位写,导致"撕裂写"(半新半旧)。JMM 规定:volatile long/double 的读写必须是原子的(JDK 5+ 强制),非 volatile 的 long/double 写入不保证原子(允许撕裂,但实现上 64 位平台通常原子)。因此共享 long/double 更新应使用 volatile 或 AtomicLong,避免读到撕裂值。现代 64 位 JVM 上非 volatile 写通常也原子,但规范不保证。

JMM 对 64 位的原子性保证仅限 volatile 读写。非 volatile 的 long/double 在 32 位平台可能撕裂,需 volatile/AtomicLong 保护。

#
★★

21. LongAdder 与 AtomicLong 的伪共享优化

LongAdder 与 AtomicLong 在伪共享优化上有何不同?

  • AtomicLong 单点
  • LongAdder 的 cell 分段
  • @Contended 填充

AtomicLong 用一个 volatile long,所有线程更新同一变量,高并发下缓存行 ping-pong 严重。LongAdder 内部维护一个 base 和 Cell[] 数组,Cell 用 @Contended 注解做了缓存行填充,每个 Cell 独占缓存行,不同线程映射到不同 Cell 更新,避免共享缓存行(伪共享),降低竞争。因此高并发多线程更新时 LongAdder 通过"分段+缓存行填充"显著优于 AtomicLong 的吞吐。

LongAdder 的伪共享优化 = 分 Cell + @Contended 缓存行填充。每个线程写自己的 Cell,互不争用同一缓存行。

#
★★

22. LongAdder.reset 与 sumThenReset 在并发更新时可能丢失怎样的观测,窗口统计应如何切换

LongAdder.reset 与 sumThenReset 在并发更新时可能丢失怎样的观测?窗口统计应如何切换?

  • reset 的并发丢失
  • sumThenReset 的原子性局限
  • 窗口统计切换策略

LongAdder.reset 将值清零,sumThenReset 返回当前值并清零。但在并发更新中,reset 可能清掉刚发生的更新,而 sumThenReset 的"求和+清零"也不是原子快照:求和过程中可能有更新丢失或计入下个窗口。用于窗口统计时,应接受"近似"语义:用 sumThenReset 在窗口边界取近似值,更新可能被切到边界两侧,但整体统计不至于严重失真。若要精确,需用锁或改为"快照叠加"(累计值的差值)而非直接清零。

LongAdder 专为"吞吐统计"设计,精确性弱。窗口统计用 sumThenReset 取近似即可,需精确时用加减差值法。

#
★★

23. LongAdder.sum 为何不是原子快照,它适合监控计数却不适合作为哪些业务判断依据

LongAdder.sum 为何不是原子快照?它适合监控计数,却不适合作为哪些业务判断依据?

  • sum 的遍历求和非原子
  • 适合监控/统计
  • 不适合精确业务判断

LongAdder.sum() 遍历 base 和所有 Cell 求和,遍历过程中 Cell 可能被并发修改,因此 sum 不是原子快照,得到的可能是"某时刻"的近似值。它适合监控指标(如 QPS、请求量趋势),因为统计允许微小误差。但不适合作为精确的业务判断依据,如:判断余额是否充足、是否达到精确阈值触发动作、对账、精确计数等,这些场景需要精确值,应改用 AtomicLong 或加锁,或使用后验证。

吞吐 vs 精确的取舍。LongAdder 牺牲 sum 的原子性换取更新吞吐,精确业务逻辑不可依赖其近似值。

#
★★

24. LongAdder/LongAccumulator(JDK 8+)在热点更新场景下对 AtomicLong 的性能优势

LongAdder/LongAccumulator(JDK 8+)在热点更新场景下对 AtomicLong 的性能优势是什么?

  • 热点更新竞争
  • 分段计数的吞吐
  • 适用场景

AtomicLong 在热点更新(多线程高频自增)下,所有线程争抢同一个 volatile 变量,CAS 竞争激烈、缓存行 ping-pong,吞吐受限。LongAdder 通过 base+Cell[] 分段,把更新分散到多个 Cell,每个 Cell 独立 CAS,竞争大幅降低,吞吐随 cell 数扩展,远高于 AtomicLong。LongAccumulator 通用化累加(可自定义函数)。适合高并发计数、状态统计、频控等热点更新场景;读取少或为保证精确则用 AtomicLong。

热点更新吞吐优势的本质是"分段分散竞争"。LongAdder 是"写多读少"场景的最优解,读时合并不精确。

#
★★

25. Striped64 的内部实现与 LongAdder 演化

Striped64 的内部实现是怎样的?LongAdder 如何基于它演化?

  • Striped64 的 base + Cell[] 结构
  • 扩容与冲突处理
  • LongAdder 对 Striped64 的封装

Striped64 是 LongAdder/LongAccumulator 的基类,维护一个 base 和 Cell[] 数组。Cell 用 @Contended 填充避免伪共享。更新时先尝试更新 base(CAS),若竞争激烈则 CAS 一个 Cell(通过线程哈希定位);若 Cell 冲突(CAS 失败)则扩容 Cell 数组或迁移到其他 Cell。自动扩容在竞争加剧时把负载分散到更多 Cell。LongAdder 只需实现 add/sum 的数值语义,复用 Striped64 的竞争分散机制,是 Striped64 的特例。

Striped64 是"分段计数"的通用引擎,处理竞争检测、扩容、哈希定位。LongAdder 继承它实现 Long 累加。

#
★★

26. VarHandle 的 acquire/release 访问与 volatile 访问在可见性、排序和性能上有什么差异,何时可以降低内存序

VarHandle 的 acquire/release 访问与 volatile 访问在可见性、排序和性能上有什么差异?何时可以降低内存序?

  • acquire/release 单向排序
  • volatile 全序
  • 性能与降低内存序的时机

volatile 访问是 strongest 内存序:读-写全序,禁止几乎全部重排。acquire/release 只约束单向:release 写保证先前操作不被重排到写之后,acquire 读保证后续操作不被重排到读之前,配对建立 happens-before,但允许更多重排,性能通常更低开销。何时降低内存序:在"发布-获取"模式中,只需单向传递时用 acquire/release;在无锁算法中,若只关心"自定义数据先于标志发布",可用 release 写 + acquire 读,不必用 volatile 全序,从而减少同步屏障、提升性能。

"降低内存序"的思想是"只建立需要的约束,浪费的顺序约束不加"。acquire/release 满足大多发布-消费场景且更省。

#
★★

27. VarHandle 的 plain、opaque、acquire、release 和 volatile 访问模式分别提供哪些排序保证

VarHandle 的 plain、opaque、acquire、release 和 volatile 访问模式分别提供哪些排序保证?

  • 五种内存序的排序保证
  • 从弱到强
  • 各自适用

五种模式从弱到强:1) plain:无任何排序保证,仅保证原子性(对 int/long/引用);2) opaque:保证原子性且禁止"out-of-thin-air"取值,但不保证与其他操作的排序(保持因果一致性);3) acquire:读语义,保证读之后的操作不被重排到读之前,与 release 配对建立 happens-before;4) release:写语义,保证写之前的操作不被重排到写之后;5) volatile:最强,读-写全序,禁止跨 volatile 重排。选择依据是"需要多强的排序与可见性"。

内存序是"排序约束的强度谱系"。opaque 保证因果一致,acquire/release 保证单向传播,volatile 保证全序,各取所需。

#
★★

28. lock-free、wait-free 与 obstruction-free 分别保证谁能取得进展,CAS 循环通常属于哪一类

lock-free、wait-free 与 obstruction-free 分别保证谁取得进展?CAS 循环通常属于哪一类?

  • 各类进展保证的定义
  • 进程(线程)进展与系统进展
  • CAS 循环的分类

lock-free:保证至少一个线程在有限步内取得进展(系统级进展),但个别线程可能饿死。wait-free:保证每一个线程都在有限步内取得进展(线程级进展),最强,无饥饿。obstruction-free:保证在无其他线程干扰时,任何线程都能在有限步内完成,若被其他线程阻塞则可能永不进展。CAS 循环(无锁算法)通常属于 lock-free:保证系统整体进展,但单个线程可能因竞争而长时间重试。LongAdder 等接近 lock-free。

三类是"复苏进展保证"的强度,wait-free > lock-free > obstruction-free。CAS 循环是典型的 lock-free,不保证单线程无饥饿。

#
★★

29. volatile 与 synchronized 的差异(原子性 vs 可见性)

volatile 与 synchronized 在原子性与可见性上有何差异?

  • volatile 只保证可见性/有序性
  • synchronized 保证互斥+可见性+原子性
  • 适用场景

volatile 只保证:1) 可见性(写对其他线程可见);2) 有序性(禁止重序)。但不保证原子性:volatile 的读-改-写(如 i++)不是原子的,多线程并发生会丢更新。synchronized 保证:1) 互斥(同一时刻仅一个线程进入);2) 可见性(锁内写入对持有锁者可见);3) 原子性(临界区整体原子)。因此仅标志位/状态发布用 volatile 就够;需要"检查-修改-写回"或复合操作原子性时必须用 synchronized 或原子类。

核心差异:volatile 解决"可见性+有序性",synchronized 额外解决"互斥+原子性"。复合操作靠 volatile 无法保证原子。

#
★★

30. 为什么 LongAdder 在高竞争计数下通常优于 AtomicLong,但其 sum() 不提供严格瞬时一致性,如何据此选型

为什么 LongAdder 在高竞争计数下通常优于 AtomicLong?其 sum() 不提供严格瞬时一致性,如何据此选型?

  • 高竞争下分段优势
  • sum 的非原子快照
  • 选型依据

高竞争下多线程更新 AtomicLong 的单一 volatile 变量,CAS 竞争与缓存行 ping-pong 严重,吞吐受限;LongAdder 用 base+Cell[] 分段,把更新分散到不同 Cell,竞争降低、吞吐高。但 sum() 遍历 Cell 求和时可能被并发修改,非原子快照,不保证严格瞬时一致。选型:写多读少、统计类(计数、QPS、监控)用 LongAdder;需要精确瞬时值、读频繁或作为业务判断依据时用 AtomicLong;兼顾两者可监测更新频率后动态选择。

选型是"更新吞吐 vs 读取精确"的权衡。LongAdder 偏热点更新,AtomicLong 偏精确读取。

#
★★

31. 伪共享如何让彼此独立的原子计数相互拖慢,缓存行填充的效果应怎样用硬件计数器验证

伪共享如何让彼此独立的原子计数相互拖慢?缓存行填充的效果应怎样用硬件计数器验证?

  • 伪共享的拖慢机制
  • 缓存行填充
  • 硬件计数器验证(PMU)

若两个独立原子变量位于同一缓存行,一个线程更新 A 会使缓存行失效,另一个线程更新 B 需重新读取缓存行,二者互相拖慢(即使逻辑上无共享)。缓存行填充(把变量隔离到独立缓存行)消除该问题。验证效果用硬件性能计数器(PMU/perf):对比填充前后的 cache miss 率、缓存行争用次数(如 x86 的 HITM/EVICT 事件)与吞吐,填充后应显著降低同缓存行争用并提升吞吐。可用 perf stat 或 JTune 等采集这些计数器。

伪共享、缓存行填充、硬件计数器三位一体。验证需用 PMU 观测 cache miss 与争用,而非仅看吞吐。

#
★★

32. 使用 AtomicReference 保存不可变聚合状态时,如何确保一次 CAS 同时维护多个字段的不变量

使用 AtomicReference 保存不可变聚合状态时,如何确保一次 CAS 同时维护多个字段的不变量?

  • 不可变聚合对象
  • 整体 CAS 替换
  • 多字段不变量

把多个字段封装进一个不可变对象(所有字段 final),用 AtomicReference 持有该对象引用。更新时读取当前引用,构造一个包含所有字段新值的新不可变对象,然后用一次 compareAndSet 整体替换引用。这样"多个字段的修改"在读者看来是原子的:要么看到旧完整状态,要么看到新完整状态,不会看到中间态(半新半旧)。这保证了多字段不变量(如 A<=B、和恒定)在一次 CAS 中成立。

关键是把"多个字段"折叠为"一个不可变引用",用 CAS 整体替换。读者读到的是不可变对象,天然一致。

class Point2D { final int x, y; Point2D(int x, int y){this.x=x;this.y=y;} }
AtomicReference<Point2D> ref = new AtomicReference<>(new Point2D(0,0));
// 原子地同时更新 x,y
for (;;) {
    Point2D cur = ref.get();
    Point2D next = new Point2D(cur.x + 1, cur.y + 1);
    if (ref.compareAndSet(cur, next)) break;
}
#
★★

33. 原子更新函数若读取外部可变状态会产生什么竞态,如何把它改造成确定性的纯函数

原子更新函数若读取外部可变状态会产生什么竞态?如何把它改造成确定性的纯函数?

  • 原子更新函数重执行
  • 读外部可变状态的竞态
  • 纯函数改造

Atomic 的 updateAndGet/getAndUpdate 传入的函数可能被重复执行(CAS 失败重试),且不保证与当前值的原子性。若函数读取外部可变状态(如共享计数器、当前时间、网络状态),则每次执行结果可能不同,且该外部状态可能在 CAS 期间变化,导致更新结果不确定甚至违反不变量。改造:让函数成为纯函数——只依赖传入的当前值参数,不读取任何外部可变状态,保证给定相同输入得出相同输出,重试时结果一致,从而保证确定性。

原子更新函数必须"只依赖参数、无副作用"。读外部可变状态违背纯函数,破坏 CAS 重试的确定性。

#
★★

34. 固定次数、指数退避和随机抖动在 CAS 高竞争下各解决什么问题,如何避免同步活锁

固定次数、指数退避和随机抖动在 CAS 高竞争下各解决什么问题?如何避免同步活锁?

  • 固定次数的限旋
  • 指数退避的降竞争
  • 随机抖动的错峰

固定次数:限制自旋次数,防止无限空转,超限转阻塞或放弃。指数退避:随着失败次数增加等待时间指数增长,降低缓存行/总线争用,减少无效失败。随机抖动:在退避中加随机量,避免多个线程同步退避后同时重试再次冲突(同步活锁)。避免同步活锁的关键是"随机化重试时机",让线程错峰而非整齐划一。三者组合:先固定次数快速尝试,失败改用指数退避+随机抖动,可有效降低高竞争下的空转与活锁。

三个策略分别解决"限旋、降争、错峰"。同步活锁靠随机抖动打破节奏对称性。

#
★★

35. 把状态位和计数打包进一个 AtomicLong 有何原子性优势,位宽、符号扩展和溢出如何处理

把状态位和计数打包进一个 AtomicLong 有何原子性优势?位宽、符号扩展和溢出如何处理?

  • 单 long 原子更新多字段
  • 位宽分配
  • 符号扩展与溢出

把多个状态位(如标志位、版本号、计数)打包进一个 long,用一次 CAS 原子更新整个 long,从而让"多字段组合"的修改原子化,避免锁定。位宽分配:按字段所需位数划分(如高 N 位状态、低 M 位计数),需保证总位数 ≤ 64。符号扩展:用 >>>(无符号右移)与 & 掩码提取字段,避免符号位扩展干扰。溢出处理:字段达到位宽上限时回绕或抛错,需在业务层判断;计数用低位、版本用高位,低位溢出常被接受(回绕)或用更大位宽。

打包到 long 的本质是"用单原子变量表达组合状态"。位运算要正确用掩码与无符号移位,并处理位宽溢出。

#
★★

36. 无锁栈仅比较头节点引用时如何遭遇 ABA,版本戳与不可复用节点分别怎样缓解

无锁栈仅比较头节点引用时如何遭遇 ABA?版本戳与不可复用节点分别怎样缓解?

  • 无锁栈的 ABA
  • 版本戳方案
  • 不可复用节点方案

无锁栈用 CAS 比较头节点引用。若线程 A 读到头为 X,线程 B 弹出 X 又压入 X(或压回相同节点),头引用恢复为 X,A 的 CAS 误判"头未变"而成功,但此时栈结构已变,导致错误。缓解:1) 版本戳(AtomicStampedReference):头节点引用+版本号打包,CAS 时同时比较版本,B 的操作改变版本号,A 的 CAS 因版本不匹配失败;2) 不可复用节点:每次压入新节点(不回收复用),弹出的节点不再被压回,头引用不会回绕到相同值。实际无锁栈常结合内存回收(Hazard Pointer/GC)与版本标记。

ABA 的根源是"引用值回绕"。版本戳用"逻辑代"区分,不可复用节点用"物理唯一"避免回绕。

#
★★

37. 用原子状态实现只执行一次的初始化时,如何表达进行中、成功和失败并保证等待者可恢复

用原子状态实现只执行一次的初始化时,如何表达进行中、成功和失败并保证等待者可恢复?

  • 状态机(未初始化/进行中/完成/失败)
  • CAS 状态转移
  • 失败重试与等待者恢复

用 AtomicInteger 或 AtomicReference 表达状态:0=未初始化、1=进行中、2=完成、3=失败。初始化线程用 CAS 从 0 到 1 抢占(只有一人成功),执行初始化;成功 CAS 1→2,失败 CAS 1→3。等待者读取状态:为 2 直接用结果;为 1 则等待(自旋/条件变量);为 3 则重试或抛异常。失败后由其他线程再次 CAS 3→1 重试,保证"只执行一次成功语义"且可恢复。用 CAS 保证状态转移原子,避免并发重复初始化。

关键在于"进行中"状态让等待者不盲目重复初始化,失败状态允许重试。CAS 状态机是实现"init-once"的正确方式。

#
★★

38. 虚拟线程数量很大时共享 CAS 热点为何仍受 CPU 核数限制,增加任务数会怎样放大竞争

虚拟线程数量很大时,共享 CAS 热点为何仍受 CPU 核数限制?增加任务数会怎样放大竞争?

  • 虚拟线程与 CPU 核数
  • CAS 热点与物理并行
  • 任务数放大竞争

虚拟线程再多,物理执行也受 CPU 核数限制,同一时刻最多核数个虚拟线程真正执行 CAS。共享 CAS 热点是单点,虚拟线程卸载后重新挂载执行时仍会争抢该热点,故吞吐受核数限制而非虚拟线程数。增加任务数:更多虚拟线程涌入同一 CAS 热点,竞争加剧,导致更多 CAS 失败重试、缓存行 ping-pong 更频繁,每个虚拟线程的延迟上升,整体吞吐不随任务数线性增长甚至下降。因此虚拟线程解决"阻塞"瓶颈,不解决"共享热点竞争"瓶颈。

虚拟线程擅长消除阻塞等待,但 CAS 热点是物理竞争点,与虚拟线程数量无关,受核数与热点本身限制。

#
★★

39. 设计一个 CAS 自旋更新循环时,如何设置重试上限、退避策略与失败指标,避免高竞争下 CPU 长时间空转

设计一个 CAS 自旋更新循环时,如何设置重试上限、退避策略与失败指标,避免高竞争下 CPU 长时间空转?

  • 重试上限
  • 退避策略
  • 失败指标与降级

设计要点:1) 重试上限:设定最大自旋次数(如 100 次),超限则放弃更新或转阻塞(park/锁),避免无限空转;2) 退避策略:失败后指数退避+随机抖动,降低争用;3) 失败指标:通过计数器统计 CAS 失败率/重试次数,超过阈值触发告警或降级(如改用锁、扩容);4) 结合 Thread.onSpinWait 提示处理器。这样在高竞争下 CPU 不会长时间空转,且能监控识别异常。

防空转 = 限旋 + 退避 + 指标兜底。自旋必须"有界",否则高竞争下 CPU 被无效重试耗尽。

#
★★

40. 评估 AtomicLong 与 LongAdder 时,为什么必须同时测量写吞吐、读取频率和结果精确性

评估 AtomicLong 与 LongAdder 时,为什么必须同时测量写吞吐、读取频率和结果精确性?

  • 写吞吐 vs 读频率
  • 结果精确性
  • 综合选型

AtomicLong 写吞吐低但读精确、读快;LongAdder 写吞吐高但读(sum)需遍历、非精确、读慢。仅测写吞吐不足以决策:若读频率很高,LongAdder 的 sum 遍历会成为瓶颈;若业务需精确值,LongAdder 不适用。因此必须同时测量:1) 写吞吐(热点更新);2) 读频率与读延迟(sum 成本);3) 结果精确性需求(是否接受近似)。综合三者决定选型,否则可能选错导致读成为新瓶颈或精度不达标。

选型是三维权衡:写、读、精度。单项测量会误导,必须结合业务读写比与精度要求。

#
★★

41. 请说明硬件 CAS 如何保证比较与写入的原子性,并结合 AtomicStampedReference 解释它怎样检测 ABA 问题

请说明硬件 CAS 如何保证比较与写入的原子性,并结合 AtomicStampedReference 解释它怎样检测 ABA 问题?

  • 硬件 CAS 原子性(LOCK 前缀/LL-SC)
  • AtomicStampedReference 的引用+戳
  • ABA 检测

硬件 CAS 通过 CPU 指令保证"比较-单次写入"的原子性:x86 用 LOCK CMPXCHG(锁定总线/缓存行),ARM 用 LL/SC(加载链接/条件存储)保证比较与写入之间不被其他线程打断。AtomicStampedReference 把"引用"与"版本戳(stamp)"打包,CAS 时同时比较引用与戳:若线程 A 读到 (X,0),线程 B 做了 X→Y→X 操作,戳变为 1,A 的 CAS 因戳不匹配(期望 0)而失败,从而检测出 ABA。它把"只比较值"升级为"比较值+版本",区分相同值的不同代。

硬件保证原子性,版本戳保证"逻辑代际"区分。两者结合让 CAS 能识别"值回绕但状态已变"。

#
★★

42. 轻量级锁(CAS + 自旋)与重量级锁(OS Mutex)的升级路径

轻量级锁(CAS + 自旋)与重量级锁(OS Mutex)的升级路径是什么?

  • 无锁 → 偏向 → 轻量 → 重量
  • 升级触发条件
  • 性能影响

synchronized 的锁升级路径:无锁 → 偏向锁(单线程)→ 轻量级锁(CAS 自旋,竞争不激烈)→ 重量级锁(OS Mutex,竞争激烈)。偏向锁在 JDK 15 默认禁用;轻量级锁用 CAS 修改 Mark Word 为锁记录指针,竞争加剧时自旋;自旋失败或竞争激烈升级为重量级锁,线程 park 阻塞、由 OS 调度。升级是"逐步放大开销":从无锁到 CAS 到阻塞,用最轻的机制满足当前竞争度。升级不可逆(重量级锁不降级)。

升级路径是 JVM 对竞争度的自适应。理解"何时升级"(自旋失败、竞争激烈)是锁性能的关键。

#
★★

43. 高基数指标为每个标签创建 LongAdder 会造成什么内存问题,如何限制标签与回收计数器

高基数指标为每个标签创建 LongAdder 会造成什么内存问题?如何限制标签与回收计数器?

  • 高基数内存膨胀
  • 标签数量限制
  • 计数器回收

高基数指标(如每个用户/请求路径一个标签)为每个标签创建 LongAdder,会创建大量对象,每个 LongAdder 含 Cell[] 数组(@Contended 填充),内存占用大且难以回收,易导致内存膨胀与 GC 压力。限制与回收:1) 限制标签基数(白名单、上限、抽样);2) 用单一 LongAdder + map 管理标签,但 map 也要控规模;3) 定期回收/归档不活跃标签(LRU、TTL 淘汰);4) 用分段但复用的计数器结构。避免"每个标签一个重量级计数器"。

高基数监控的核心是"控制标签数量与回收策略"。LongAdder 昂贵,不能无限制为每个标签实例化。

#
★★

44. 高并发使用 AtomicInteger 计数时可能出现哪些伪共享现象,@Contended 的使用限制和验证方法是什么

高并发使用 AtomicInteger 计数时可能出现哪些伪共享现象?@Contended 的使用限制和验证方法是什么?

  • 相邻 AtomicInteger 的伪共享
  • @Contended 限制
  • 验证方法

多个 AtomicInteger(或与其它热字段)位于同一缓存行,不同线程更新它们时互相失效缓存行,造成伪共享、吞吐下降。@Contended 使用限制:1) 需 JVM 参数 -XX:-RestrictContended(默认仅 JDK 内部可用)才能用于用户代码;2) 增加内存占用(每字段填充到 64 字节);3) 只影响字段布局,需对象对齐。验证方法:对比加/不加 @Contended 的吞吐与 cache miss(用 perf/PMU 计数器),加后应显著提升吞吐并降低缓存行争用。

伪共享、@Contended 与验证三者联动。@Contended 有 JVM 限制且增加内存,验证需用硬件计数器。

#
★★

45. Atomic 类与 PhantomReference 的边界

Atomic 类与 PhantomReference 的边界在哪里?

  • Atomic 类的职责
  • PhantomReference 的职责
  • 两者协作场景

Atomic 类负责"并发控制":对值/引用做原子更新与 CAS,保证线程安全。PhantomReference 负责"对象回收后清理":在对象被 GC 回收后(finalize 后、真正释放前)触发清理动作,用于跟踪对象生命周期、资源回收(如堆外内存、Native 资源)。两者边界:Atomic 管"并发正确性",PhantomReference 管"回收时机"。协作场景:无锁结构(如 Lock-Free 栈)用 Atomic 做指针更新,用 PhantomReference 或类似机制在节点回收时清理资源,防止内存泄漏。

边界清晰:Atomic 控制并发访问,PhantomReference 控制回收时机。二者可配合实现"并发安全 + 安全回收"。

#
★★

46. Atomic 类的 lazySet 与 set 的差异(内存序)

Atomic 类的 lazySet 与 set 有何差异(内存序)?

  • lazySet 的弱序(release 语义)
  • set 的 volatile 写
  • 性能差异

set 是 volatile 写,具备强内存序(全序),写入对其他线程立即可见且禁止重排。lazySet 是"宽松"写(release 语义,底层用 Unsafe.putOrdered 或 VarHandle.setRelease),只保证写入最终可见且不重排到之前的写之前,但不保证立即可见(允许延迟到后续写),且不建立全序屏障。性能上 lazySet 通常更快(少一次 store barrier)。适用场景:对"值最终会被读到、无需立即可见"的场景(如队列里的槽位、仅用于终止标志的写)用 lazySet 降低开销。

lazySet 牺牲"及时可见性"换取性能,本质是 release 写。适用于延迟可见可接受的场景。

#
★★

47. AtomicInteger.getAndUpdate 的函数可能被重复执行,为何其中不能包含发送消息等副作用

AtomicInteger.getAndUpdate 的函数可能被重复执行,为何其中不能包含发送消息等副作用?

  • 函数重试
  • 副作用问题
  • 无副作用要求

getAndUpdate 调用下更新函数,若 CAS 失败会重试并再次调用该函数。若函数内含副作用(发送消息、写日志、计数、IO),每次重试都会重复执行副作用,导致消息重复发送、日志重复、计数错误。因此函数必须是纯函数(无副作用),只基于入参计算新值。若确实需要副作用,应先计算好结果再原子更新,或把副作用放在 CAS 成功之后执行。

原子更新函数"可能重复执行"是设计前提,副作用违背该前提,必须把副作用移出函数。

#
★★

48. AtomicInteger、AtomicLong、AtomicBoolean 的常用 API

AtomicInteger、AtomicLong、AtomicBoolean 的常用 API 有哪些?

  • 读取/设置
  • 原子增减
  • CAS 与更新函数

三者常用 API:get()/set()/lazySet()(读取/写入);getAndIncrement()/getAndAdd()/incrementAndGet()/decrementAndGet()(原子增减);compareAndSet()/weakCompareAndSet()(CAS);getAndUpdate()/updateAndGet()/getAndAccumulate()/accumulateAndGet()(函数式更新);AtomicBoolean 有 getAndSet()/compareAndSet() 用于布尔标志。AtomicInteger/Long 还支持 compareAndExchange()(返回 witness 值)。这些都是基于 volatile 值 + CAS 的原子操作。

Atomic 类 API 覆盖"基本读写、原子增减、CAS、函数更新"四类,是并发计数与标志位的标准工具。

#
★★

49. AtomicLong 计数溢出不会抛异常,长期运行的序列号或配额系统应如何检测并定义回绕语义

AtomicLong 计数溢出不会抛异常,长期运行的序列号或配额系统应如何检测并定义回绕语义?

  • AtomicLong 溢出行为
  • 回绕检测
  • 回绕语义定义

AtomicLong 在累加溢出时会回绕到负数(Long.MIN_VALUE...),且不抛异常。长期运行的序列号/配额系统需主动检测:1) 用 getAndAdd 后检查是否从正变负(检测溢出);2) 使用带符号判断或单独的溢出标志;3) 定义回绕语义:序列号溢出后是否允许复用(通常需保证唯一性,回绕需配合时间戳/版本);4) 用版本+低位组合(如把长期计数拆成高位段)或在接近上限时告警/重置。避免把"溢出"当作"已满"。

溢出回绕是静默的,必须显式检测并定义语义(复用、告警、重置),否则配额/序列号会出错。

#
★★

50. AtomicMarkableReference 只有布尔标记时适合表达什么状态,为何不能替代通用版本计数

AtomicMarkableReference 只有布尔标记时适合表达什么状态?为何不能替代通用版本计数?

  • 布尔标记的语义
  • 适用场景(标记位)
  • 局限 vs 版本计数

AtomicMarkableReference 把引用与一个布尔标记打包,只能表达"标记/未标记"两种状态,适合表达"是否被标记"(如节点是否已删除、是否已处理、是否已失效)这类二值状态。它不能替代通用版本计数(AtomicStampedReference),因为布尔标记只能区分"两种状态",无法表达多次变化(如 A→B→C→A 需要多代版本)。当需要区分"多次回绕"或"多步变化"时,必须用整数版本戳。

布尔标记是"二值版本",够用则用(省位),需区分多次变化时用整数版本计数。

#
★★

51. AtomicReferenceFieldUpdater 在框架中的应用

AtomicReferenceFieldUpdater 在框架中如何应用?

  • 字段级原子更新
  • 避免对象分配
  • 框架中的实例

AtomicReferenceFieldUpdater 对类的引用字段提供原子更新(CAS),无需为该字段创建 AtomicReference 对象,节省内存与分配。框架中常用于:1) 高性能缓存/无锁结构里对共享引用字段做原子更新(如状态指针、单例引用);2) 避免每个实例都持有 AtomicReference 对象(减少对象头与分配);3) 在 JDK 内部(如 ConcurrentHashMap 的某些字段)使用。要求字段 volatile 且可访问。相比 AtomicReference,它把"原子性"加到字段上而非对象上。

AtomicReferenceFieldUpdater 的价值是"零对象开销的字段原子化",适合对象实例多、字段需原子的场景。

#
★★

52. AtomicStampedReference 与 AtomicMarkableReference 都能应对 ABA 时,版本戳和布尔标记分别适合哪些业务场景

AtomicStampedReference 与 AtomicMarkableReference 都能应对 ABA 时,版本戳和布尔标记分别适合哪些业务场景?

  • 版本戳场景
  • 布尔标记场景
  • 选择依据

版本戳(整数 stamp)适合需要区分"多次状态变化"的场景,如无锁栈/队列的多次 push/pop、一次缓存条目被多次更新、需要精确代际的场景。布尔标记适合只关心"是否发生过一次关键变化"的场景,如节点是否被标记删除、任务是否已处理、资源是否已释放,只需二值状态即可,且更省空间(引用+1 位)。选择依据:需要区分几代变化用版本戳,只需"是否"语义用布尔标记。

版本戳表达"多代",布尔标记表达"二值"。按状态需要区分的粒度选择。

#
★★

53. AtomicStampedReference 的版本号最终可能回绕,超长生命周期系统应如何评估残余 ABA 风险

AtomicStampedReference 的版本号最终可能回绕,超长生命周期系统应如何评估残余 ABA 风险?

  • 版本号回绕
  • 回绕时机
  • 残余 ABA 风险评估

AtomicStampedReference 的版本号是 int(32 位),最终可能回绕。若在回绕周期内某个引用值恰好回到相同值且版本号也相同,则残余 ABA 可能发生。超长生命周期系统应评估:1) 回绕周期(2^32 次变化)是否远大于业务可观察的 ABA 窗口;2) 若变化极高频(如每秒百万次),回绕周期可能缩短到可观测时间,需扩大版本位宽(用 long 或复合结构);3) 结合"不可复用节点"降低引用值回绕可能;4) 用更强机制(如哨兵/代际+引用唯一)消除残余风险。大体上,只要版本号在"ABA 窗口"内不重复,即可接受。

版本回绕是"降低概率"而非"完全消除"。评估窗口期与回绕周期的关系,必要时扩位宽或配合不可复用节点。

#
★★

54. AtomicStampedReference 解决 ABA 问题的版本机制

AtomicStampedReference 解决 ABA 问题的版本机制是什么?

  • 引用+版本戳打包
  • CAS 同时比较
  • 版本递增

AtomicStampedReference 内部把"引用"和"版本戳(int)"打包成一个对象(或两个字段),所有操作(get、compareAndSet)同时读写两者。更新时,compareAndSet(expectedRef, newRef, expectedStamp, newStamp) 要求"当前引用==expectedRef 且当前戳==expectedStamp"才原子替换为 (newRef,newStamp)。每次修改引用时递增版本戳,因此即使引用值回绕到相同,版本戳不同,CAS 会失败,从而识别 ABA。版本戳由调用方管理(通常是修改时 stamp+1)。

版本机制 = "引用 + 代际戳"配对比较。引用可回绕,但戳不同代,CAS 就不再误判"未变"。

#
★★

55. DoubleAdder 的演进与使用场景

DoubleAdder 的演进与使用场景是什么?

  • DoubleAdder 的基础
  • 与 LongAdder 的对应
  • 浮点累加场景

DoubleAdder 是 JDK 8 引入的基于 Striped64 的 double 累加器,与 LongAdder 对应,内部用 base + Cell[] 分段,把 double 累加分散到多个 Cell,降低高竞争下的争用。它继承 Striped64 的机制(Cell 用 @Contended 填充),只是把 Cell 的 value 从 long 改为 double。使用场景:高并发下大量 double 累加(如统计请求耗时总和、吞吐量、平均值聚合),需要高写入吞吐且允许 sum() 近似。JDK 25 中 DoubleAdder 仍沿用 Striped64 机制,无重大变化。

DoubleAdder 是 Double 版的 LongAdder,适用于"高并发浮点累加 + 近似读取"的统计场景。

#
★★

56. FieldUpdater(AtomicIntegerFieldUpdater)的反射与安全

FieldUpdater(AtomicIntegerFieldUpdater)的反射与安全边界是什么?

  • 反射定位字段
  • 访问权限
  • 安全限制

AtomicIntegerFieldUpdater 通过反射定位目标字段(newUpdater 时传类与字段名),要求字段 volatile、类型匹配、可访问。安全边界:在模块化(JPMS)下,跨模块访问私有字段受模块边界限制,需导出或 setAccessible;无权限时会抛异常或无法构造。它不进行字段值校验(不保证更新值合法),直接做 CAS。相比反射直接调用,FieldUpdater 更安全高效(缓存了字段偏移),但仍是"反射式"工具,需在构建期保证字段规范。

FieldUpdater 的反射与安全边界 = 字段可见性、模块边界、构造期校验。需保证字段规范满足要求。

#
★★

57. JDK 25 中 AtomicReferenceArray 的分段伪共享填充策略

JDK 25 中 AtomicReferenceArray 的分段伪共享填充策略是什么?

  • 数组元素的伪共享
  • 分段/填充策略
  • 与批量更新的关系

AtomicReferenceArray 对数组元素做原子操作,但数组元素天然紧密排列,若多个线程频繁更新相邻元素,会共享同一缓存行导致伪共享。JDK 25 中,AtomicReferenceArray 本身不强制为每个元素填充(那会浪费内存),伪共享缓解策略通常由使用者/框架层实施:如把元素按"缓存行大小"分散(稀疏索引),或用分段结构。JDK 25 的 AtomicReferenceArray 仍以"单元素原子 + 紧凑布局"为默认,伪共享优化需按场景显式设计(如 LongAdder 用 Cell[] + @Contended 的思路)。

数组元素原子但不自动防伪共享。需要时通过"稀疏索引/分段填充"减少相邻元素争用。

#
★★

58. JDK 25 中 VarHandle 替代 Unsafe 的原子访问 API 设计

JDK 25 中 VarHandle 如何替代 Unsafe 的原子访问 API?

  • VarHandle 的封装
  • 内存序参数
  • 签名多态

VarHandle 是 JDK 9 引入、JDK 25 中全面替代直接使用 Unsafe 的官方原子访问 API。它提供对字段/数组元素的原子操作(compareAndSet、getAndAdd、getAndSet 等)与内存序控制(plain/opaque/acquire/release/volatile),并把 variable type 与 coordinate types 通过签名多态在调用点解析。相比 Unsafe 的裸 native 方法,VarHandle 更安全、类型化、可移植,且同样高性能(JIT 内联)。JDK 内部数据结构(ConcurrentHashMap、Striped64 等)已迁移到 VarHandle。

VarHandle 是 Unsafe 原子能力的官方替代,统一了"原子性 + 内存序 + 类型安全",是 JDK 25 的标接通路。

#
★★

59. Java 25 中 Atomic 类的进一步演进

Java 25 中 Atomic 类的进一步演进是什么?

  • 底层迁移到 VarHandle
  • 新 API 与语义
  • 稳定性

Java 25 中 Atomic 类底层已迁移到 VarHandle 实现(而非 Unsafe),保持对外 API 稳定。演进方向:weakCompareAndSet 语义明确(JDK 9 起与 compareAndSet 等同为 volatile 语义,弱语义用 VarHandle.weakCompareAndSetPlain);增加 compareAndExchange(返回 witness 值);内存序控制更精细。整体上 Atomic 类继续稳定,能力向其底层 VarHandle 的"原子+内存序"模型对齐,未来新能力(如更细粒度内存序)主要通过 VarHandle 暴露而非 Atomic 类。

Atomic 类演进是"内部用 VarHandle、API 稳定、语义更清晰"。新并发能力走 VarHandle 通道。

#

60. VarHandle 的 coordinate types 与 variable type 不匹配时会发生什么,调用模式为何是签名多态的

VarHandle 的 coordinate types 与 variable type 不匹配时会发生什么?调用模式为何是签名多态的?

  • coordinate types 与 variable type
  • 不匹配的异常
  • 签名多态

VarHandle 的 variable type 是操作目标的类型(如 int[] 的 int),coordinate types 是定位目标所需的参数类型(如数组的 int[] + int 索引)。调用时若传入的 coordinate 实参类型不匹配,会在运行期抛出 ClassCastException 或 WrongMethodTypeException。VarHandle 的访问方法(如 get、compareAndSet)是签名多态(signature polymorphic):编译器按调用点实参类型做类型推断,JVM 在调用点做类型检查与动态 dispatch,从而支持不同 coordinate 类型组合,无需为每种组合生成方法。这让 VarHandle 能通用地处理任意字段/数组访问。

签名多态让 VarHandle 一个方法适配所有变量类型+坐标类型,类型检查推迟到调用点,换来灵活性与性能。

#

61. VarHandle 的原子模式(AtomicAccess)在 JDK 25 中的方法签名与性能特征

VarHandle 的原子模式(AtomicAccess)在 JDK 25 中的方法签名与性能特征是什么?

  • 原子访问方法签名
  • 性能特征
  • 与 counter/累加比较

VarHandle 的原子访问方法(compareAndSet、compareAndExchange、getAndSet、getAndAdd、getAndUpdate、getAndIncrement 等)签名以 (variableType, coordinateTypes..., valueTypes...) 形式,通过签名多态在调用点解析。性能特征:JIT 会将这些方法内联为底层原子指令(如 lock cmpxchg),开销接近原生 CAS;不同内存序(volatile/acquire/release/opaque)影响屏障数量,性能各有差异。JDK 25 中 VarHandle 原子访问是 JDK 数据结构(ConcurrentHashMap、Striped64 等)的内部实现基础,性能与手写 Unsafe 相当但更安全。

VarHandle 原子访问性能接近原生 CAS,靠 JIT 内联与签名多态,是 JDK 并发结构的底层基石。

#

62. VarHandle.fullFence、acquireFence 和 releaseFence 分别约束哪些前后操作,何时需要显式栅栏

VarHandle.fullFence、acquireFence 和 releaseFence 分别约束哪些前后操作?何时需要显式栅栏?

  • fullFence 全序
  • acquireFence/releaseFence 单向
  • 显式栅栏场景

fullFence:完全内存屏障,约束其前后所有操作的顺序与可见性(全序,最重)。releaseFence:release 语义的写屏障,确保之前的内存操作不被重排到该栅栏之后(用于发布)。acquireFence:acquire 语义的读屏障,确保之后的内存操作不被重排到该栅栏之前(用于获取)。显式栅栏用于:1) 需要手动控制内存序但非标准字段访问能覆盖的场景(如自定义无锁算法、裸数组/外部内存);2) 需要"发布"或"获取"但无 volatile 字段可用时;3) 需要按需插入屏障而非依赖字段 volatile 语义。过度使用会损性能,通常优先用字段的 volatile/acquire/release 访问。

显式栅栏是"手动注入内存序"的杠杆,用于标准访问无法覆盖的自定义场景,需谨慎用。

#

63. 使用 AtomicIntegerFieldUpdater 更新对象字段需要满足哪些字段可见性、类型和继承条件,为什么不直接使用 AtomicInteger

使用 AtomicIntegerFieldUpdater 更新对象字段需要满足哪些字段可见性、类型和继承条件?为什么不直接使用 AtomicInteger?

  • 字段可见性/类型/继承条件
  • 与 AtomicInteger 的对比
  • 内存与对象开销

AtomicIntegerFieldUpdater 要求:字段 volatile(可见性)、类型 int(或对应的 long/引用)、可访问(public 或同包/模块导出)、且对该对象实例可见;继承场景下字段需属于目标实例类或父类可访问字段。直接用 AtomicInteger 更简单,但每个实例多一个 AtomicInteger 对象(对象头+字段),内存开销大;FieldUpdater 是静态的、把原子性映射到字段上,零实例对象开销,适合大量实例、字段需原子的场景。代价是限制多(volatile、可访问性)且需谨慎。

选 FieldUpdater 还是 AtomicInteger 是"内存 vs 便利/限制"的权衡。大批量实例用 FieldUpdater 省内存。

#

64. 如何用 AtomicReference 和不可变对象实现多字段一致更新,并避免读者观察到半更新状态

如何用 AtomicReference 和不可变对象实现多字段一致更新,并避免读者观察到半更新状态?

  • 不可变对象
  • 整体 CAS 替换
  • 半更新状态避免

把所有相关字段封装进不可变对象(字段 final),用 AtomicReference 持有该对象。更新时:读取当前引用 → 构造包含全部新字段的不可变新对象 → 一次 CAS 整体替换引用。读者通过 get() 读取引用,总能拿到一个完整不可变对象(旧或新),不会看到"部分字段已更新、部分未更新"的半更新状态,因为字段修改在单次 CAS 中原子完成。并发更新时 CAS 失败重试,确保基于最新状态构造。

半更新状态被"不可变对象 + 单次 CAS 替换引用"消除。读者永远看到完整的旧状态或新状态。

#

65. JDK 25 中 AtomicReference 的 compareAndSet 与 weakCompareAndSet 语义差异

JDK 25 中 AtomicReference 的 compareAndSet 与 weakCompareAndSet 语义差异是什么?

  • compareAndSet 强语义
  • weakCompareAndSet 语义
  • VarHandle 弱版本

JDK 25 中,AtomicReference.compareAndSet 保证强 CAS 语义:成功时具 volatile 全序可见性,失败确定性返回。weakCompareAndSet 自 JDK 9 起实际语义与 compareAndSet 相同(都改为 volatile 语义,实现为同一个方法),历史文档中的"弱语义"被移除;真正弱语义(允许伪失败、宽松内存序)需使用 VarHandle.weakCompareAndSetPlain 等方法。因此两者在 JDK 25 中操作上等价,但 API 命名保留,弱版本走 VarHandle。

JDK 9 起 weakCompareAndSet 与 compareAndSet 语义合并为 volatile 强语义,弱 CAS 迁移到 VarHandle,避免混淆。