← 每日面试题 学习看板

如何保证多线程下i++结果正确?

面试题/00java/02Java 并发编程/09编程实践与应用场景/如何保证多线程下i++结果正确?.md

完整讲解(权威版)

最后更新:2026-10-28 · 基于第 1 次作答

L1 · 30 秒速记

结论:i++ 非原子(读-改-写三步),多线程下保证正确本质是把"读-改-写"互斥或原子化——原子类/CAS、加锁、高并发用 LongAdder;volatile 只保可见性,单独用不够。 关键词:AtomicInteger · CAS 自旋 · LongAdder 分段 · volatile 边界 边界:只保单变量;高竞争换 LongAdder;ABA 用版本号;复合操作加锁。

L2 · 核心讲解

核心考点定位:考察 i++ 为何丢更新(读-改-写三条指令非原子),以及原子类/CAS、加锁、LongAdder 三种方案的边界差异;重点排除"volatile 能保证 i++ 原子"的误判,并追问高并发下的选型与 ABA 治理。

机制链(≥3 环):

  1. i++ 编译后非单条指令 → 底层是"读值-加一-写回"三步:两个线程同时读到旧值 0,各自写回 1,i 只增加了一次,发生丢更新。
  2. 丢更新 → 需要让"读-改-写"整体不可分割:用 synchronized/ReentrantLock 串行化临界区,或用 CAS 原子指令保证"比较-交换"一气呵成,每次 i 都精确 +1。
  3. CAS 依赖 cmpxchg 原子指令 → 高竞争下失败重试(自旋)空转烧 CPU:改用 LongAdder 分段 Cell 数组分散热点,或退化为锁。
  4. volatile 只保证可见性与有序性 → 没有互斥语义,修饰的 i 并发自增仍会丢更新:必须配合原子类或锁。

证据锚点:

对比与权衡:

方案正确性吞吐/成本失败模式适用边界
AtomicInteger / CAS单变量原子低竞争无锁、快高竞争自旋烧 CPU、ABA单变量低竞争
LongAdder原子但 sum 弱一致高并发吞吐最高sum() 非强一致高频计数、读多写少
synchronized / Lock互斥、强一致阻塞、上下文切换成本死锁、停顿复合操作、高竞争
volatile不保证原子低、内存屏障并发自增仍丢更新仅可见性标志、发布

常见误区:

  1. 错误:"volatile 能保证 i++ 原子" → 正确:volatile 只保证可见性与有序性,i++ 是复合操作,并发下仍会丢更新 → 为什么:volatile 没有互斥语义,"读-改-写"三条指令仍可被打断。
  2. 错误:"CAS 一定比锁快" → 正确:低竞争时快,高竞争时自旋空转反而更慢 → 为什么:CAS 是乐观重试,竞争激烈时重试次数暴涨,空转烧 CPU。
  3. 错误:"AtomicInteger 在高并发计数下就够用" → 正确:高频计数应换 LongAdder,分段 Cell 分散热点 → 为什么:全局 CAS 都打在同一个 value 上,LongAdder 分散到多个 Cell,竞争大幅下降。
  4. 错误:"ABA 问题业务上很少见" → 正确:链表节点复用、版本号回退等场景会出现,用 AtomicStampedReference 加版本号 → 为什么:值 A→B→A 后 CAS 会误判为"未被修改",隐蔽且危险。

适用边界:

延伸追问(≥3)

  1. 为什么 i++ 在多线程下会丢更新?
  1. LongAdder 为什么比 AtomicLong 快?
  1. CAS 的 ABA 怎么解决?
  1. volatile 到底能保证什么?

面试者参考回答

【结论】 i++ 不是原子操作,多线程下保证正确的本质是把"读-改-写"整体互斥或原子化,三条主线:原子类/CAS、加锁、高并发用 LongAdder;volatile 只保可见性,单独修 i++ 不够。

【三点】

第一,原子类与 CAS:AtomicInteger.incrementAndGet() 靠 cmpxchg 原子完成"比较-交换",单变量低竞争最快;但高竞争时自旋空转烧 CPU,且存在 ABA 问题(用 AtomicStampedReference 版本号解决)。

第二,加锁:synchronized/ReentrantLock 串行化临界区,正确性最强,但有阻塞与上下文切换成本,适合复合操作或高竞争场景。

第三,LongAdder 与 volatile:高频读多写少的计数用 LongAdder(分段 Cell 分散热点,sum() 弱一致);volatile 只保证可见性与有序性,不保证原子,不能单独用来修 i++。

【边界】 三点提醒:一是只保单变量原子性,复合操作要 AtomicReference 打包或加锁;二是高竞争时 CAS 自旋反而更慢,换 LongAdder 或退化为锁;三是 volatile 不能替代互斥,只能做可见性标志。

面试记录 2026-10-28(第 1 次)

题目:如何保证多线程下i++结果正确?

难度:medium

标签:java/concurrency-practice, java/concurrency, general/knowledge, difficulty/medium

评分:⭐⭐⭐☆☆(3/5)

我的回答

多线程下保证i++正确可以有多种方案,i++命令并不是一个原子命令,如果只是要实现i+1的操作,可以直接用jdk的原子类内部自带加1等的操作,可以保证原子性的加减;另外可以使用CAS,通过比较交换来实现i++操作;最后是加锁,让所有线程串行化执行,来让i++在多线程下正确执行

点评

评分:⭐⭐⭐☆☆(3/5)

优点:三方案覆盖全(原子类/CAS/加锁)、点出 i++ 非原子、有"串行化"意识。

不足:没讲 CAS 的 ABA 问题与自旋开销(高竞争下自旋浪费 CPU,可退化到锁或 LongAdder 分段累加);没讲 LongAdder 适合高频读多写少的计数场景(分段 Cell 数组 + 最后 sum());没讲 volatile 单独不能保证 i++ 原子(只能保证可见性)。

补充:①volatile 只保证可见性不保证复合原子性;②高竞争计数用 LongAdder(分段累加)比 AtomicLong 自旋更优;③CAS 注意 ABA(可用 AtomicStampedReference)。

完整讲解见上方「完整讲解(权威版)」