2.1 java 面试题:并发锁
CAS(Compare And Swap,比较并交换)是并发编程中无锁化实现的基石。它是 CPU 层面提供的一条原子指令,Java 通过Unsafe类来调用它,从而构建出AtomicInteger、AQS锁、ConcurrentHashMap等整个 JUC 并发包。
老练的 Java 工程师不能只背“比较并替换”这五个字,要能讲清楚:硬件如何保证原子性、Java 如何封装、以及它有什么致命缺陷。
一、CAS 到底在做什么?—— 用大白话讲
想象你去银行保险库取钱。保险库的规则是:你必须确认钱箱里的钱,和你上次看到的金额一模一样,才能把钱拿走,否则就重新确认。
这就对应 CAS 的三个操作数:
- 内存位置(V):钱箱里的钱。
- 旧的预期值(A):你上次看到的金额。
- 要更新的新值(B):你想把钱箱的金额改成多少。
操作过程:
比较V和A是否相等 → 如果相等,V没被别人动过,交换成B;如果不相等,说明有人抢先改了,本次操作失败,需要重试。
二、CAS 在硬件层面如何保证原子性?
“比较”和“交换”是两个动作,CAS 怎么保证它们之间的原子性呢?
答案是:这是一条 CPU 指令完成的。
- x86 架构:对应
CMPXCHG指令,配合LOCK前缀可以锁定总线或缓存行,保证在多核 CPU 下的原子性。 - ARM 架构:对应
LDREX/STREX指令对(加载链接 / 条件存储)。 - Java 视角:
Unsafe.compareAndSwapInt()是一个native方法,直接编译成上述 CPU 指令,在指令级别是原子的,不会被线程中断。
三、CAS 在 Java 中如何工作?(结合 AtomicInteger 源码)
AtomicInteger.incrementAndGet()是理解 CAS 的最佳入口。
// AtomicInteger.javaprivatevolatileintvalue;publicfinalintincrementAndGet(){// 调用 Unsafe 的 getAndAddInt,然后 +1returnunsafe.getAndAddInt(this,valueOffset,1)+1;}核心在Unsafe.getAndAddInt中:
// sun.misc.Unsafe.javapublicfinalintgetAndAddInt(Objecto,longoffset,intdelta){intv;do{// 1. 从主存读取当前值(volatile 保证可见性)v=this.getIntVolatile(o,offset);// 2. 尝试 CAS:比较内存值是否还是 v,是则更新为 v+delta}while(!this.compareAndSwapInt(o,offset,v,v+delta));returnv;// 返回旧值}执行流程:
- 读取:拿到当前内存值
v。 - CAS 尝试:原子地判断内存值是否仍等于
v。如果相等,更新为v+1,循环结束;如果不等,说明被其他线程改过。 - 自旋重试:CAS 返回
false,则重新读取最新值,再试一次,直到成功。
这就是自旋锁的思想:宁可循环等待(占用少量 CPU),也不让线程挂起(避免昂贵的上下文切换)。
四、CAS 的三大缺陷(面试高分点)
1. ABA 问题
现象:线程 1 读到 A,准备 CAS 时,线程 2 把 A 改成 B,又改回 A。线程 1 的 CAS 仍然成功,但它不知道中间发生过变化。
举例:银行账户余额 100 元。你准备取出 50 元,在操作间隙,别人转入 200 元又转出 200 元,余额仍是 100 元。你 CAS 成功取出 50 元,没问题。但如果这是一个链表操作(如并发栈),ABA 可能导致节点指针错乱,造成严重 Bug。
解决:AtomicStampedReference(加版本号)或AtomicMarkableReference(加布尔标记)。
2. 自旋开销大
现象:当并发极高、竞争激烈时,CAS 会反复失败,线程一直在do...while循环里空转,消耗大量 CPU。
解决:
- 低竞争下:CAS 性能远超
synchronized(无上下文切换)。 - 高竞争下:可以用
LongAdder(JDK 8+),它将热点值分散到多个 Cell,最后求和,减少竞争;或者退回到synchronized(JDK 6+ 经过锁升级优化,在激烈竞争下反而更好)。
3. 只能保证一个变量的原子性
CAS 只能对一个内存位置进行原子操作。如果需要对多个变量同时操作,比如“原子地更新账户 A 和账户 B 的余额”,CAS 无能为力,必须用synchronized、ReentrantLock或分布式锁来保护整个代码块。
五、CAS 在 JUC 中的广泛应用(展现你的全局观)
CAS 不只是AtomicInteger,整个 JUC 包都是建立在 CAS 之上的:
- AQS(AbstractQueuedSynchronizer):
ReentrantLock、CountDownLatch、Semaphore底层都用 CAS 来修改state变量,实现加锁/释放锁的原子操作。 - ConcurrentHashMap:JDK 8 中用 CAS 向空桶写入头节点,替代了以前的分段锁。
- 线程池:
ThreadPoolExecutor用 CAS 来原子地修改ctl字段(工作线程数和线程池状态打包在一起)。 - 自旋锁:JDK 1.6 引入的轻量级锁,本质就是通过 CAS 在对象头的 Mark Word 上自旋尝试加锁。
六、面试话术模板
“CAS 是无锁编程的基石。它是一条 CPU 级别的原子指令,在 x86 上是带 LOCK 前缀的 CMPXCHG 指令。Java 通过 Unsafe 的 native 方法来调用它。
核心思想是:比较内存当前值是否等于旧预期值,相等就更新,不等就自旋重试。它解决的是单变量原子操作的问题,优点是轻量、无锁、无上下文切换;缺点也很明显:ABA 问题、高竞争自旋开销大、只能管一个变量。
在银行系统里,我用
AtomicInteger做交易计数器、日切开关,这些场景竞争小、CPU 承受得起自旋。但对于资金余额这种强一致性要求的操作,我绝不用 CAS 裸写,而是用ReentrantLock或分布式锁,确保业务逻辑的完整原子性。”
这样回答,既有硬件原理,又有 Java 实现,还有缺陷分析和场景取舍,能充分展现你对并发编程的深度理解。
在 Java 锁的体系中,AtomicInteger代表的是乐观锁(无锁)的实现。它并不是通过传统的synchronized或Lock接口来阻塞线程,而是利用CAS(Compare And Swap)算法来保证数据的原子性,从而在极高并发下获得更好的性能。
老练的 Java 工程师要能从“锁的演进”角度讲清楚:从悲观锁到乐观锁,从重量级到轻量级,AtomicInteger 为什么快,以及有什么坑。
一、AtomicInteger 解决了什么问题?
我们知道多线程下执行count++是不安全的,因为count++实际上是三步:读、改、写。
传统方案是加synchronized:
privateintcount=0;publicsynchronizedvoidincrement(){count++;}但这样每次操作都要加锁,造成线程阻塞和上下文切换。AtomicInteger则提供了一种非阻塞的原子操作方案:
privateAtomicIntegercount=newAtomicInteger(0);publicvoidincrement(){count.incrementAndGet();}它保证incrementAndGet()整个操作是原子的,且线程不会被阻塞。
二、AtomicInteger 的原理(CAS + volatile)
AtomicInteger内部维护一个volatile int value(保证内存可见性),核心操作通过Unsafe 类的compareAndSwapInt方法实现。
CAS 算法思想:
- 读取当前值
current。 - 计算目标值
next = current + 1。 - 原子地比较内存中的值是否仍是
current,如果是,则更新为next;如果不是(说明被其他线程改过),则重新读取,重复步骤 1~3(自旋)。
源码层面(简化版incrementAndGet()):
publicfinalintincrementAndGet(){returnunsafe.getAndAddInt(this,valueOffset,1)+1;}// Unsafe.getAndAddInt 核心逻辑publicfinalintgetAndAddInt(Objecto,longoffset,intdelta){intv;do{v=getIntVolatile(o,offset);// 读取当前值}while(!compareAndSwapInt(o,offset,v,v+delta));// CAS 尝试更新returnv;}整个过程没有加锁,只有一个do...while循环,失败就重试,所以称为自旋。
三、AtomicInteger 在“锁”体系中的定位
| 对比维度 | synchronized | ReentrantLock | AtomicInteger |
|---|---|---|---|
| 锁类型 | 悲观锁(阻塞) | 悲观锁(可阻塞/非阻塞) | 乐观锁(无锁,CAS 自旋) |
| 性能 | 高并发下上下文切换开销大 | 比 synchronized 灵活,但仍有切换 | 极高并发下性能最优(无上下文切换) |
| 适用场景 | 一般互斥,代码块较复杂 | 需要可中断、超时、公平等 | 单一变量的原子操作(计数器、标志位) |
| 缺点 | 阻塞时间长时浪费 CPU | 仍需挂起线程 | 自旋消耗 CPU,功能单一,有 ABA 问题 |
关键理解:AtomicInteger 不是锁,它只是用 CAS 实现了原子性,是“锁的替代品”中最轻量的一种。
四、银行场景下的典型应用
在银行核心系统中,AtomicInteger 很适合高频计数和无锁状态标记:
统计接口调用量 / 交易量:
privateAtomicIntegertxCount=newAtomicInteger(0);voidprocessTx(){txCount.incrementAndGet();}用于监控和限流,性能几乎无损耗。
并发控制标志位(如日切状态):
privateAtomicIntegerdaySwitch=newAtomicInteger(0);voidswitchToNextDay(){if(daySwitch.compareAndSet(0,1)){// 执行日切}}序列号生成器(局部):
AtomicIntegerseq=newAtomicInteger();StringnextId="TX"+System.currentTimeMillis()+seq.getAndIncrement();但注意,分布式环境要用分布式 ID 生成器(如雪花算法)。
五、重要特性与注意事项(展示你的深度)
1. ABA 问题
CAS 判断值未变就更新,但可能值从 A 变为 B 又变回 A,CAS 无法察觉。
解决:使用AtomicStampedReference或AtomicMarkableReference增加版本号。
2. 自旋 CPU 消耗
高竞争下,CAS 会反复失败自旋,反而浪费 CPU。
对策:竞争激烈时建议用LongAdder(JDK 8+),它内部将热点值分散到多个 Cell,最后求和,吞吐量更高。
3. 只能操作单一变量
AtomicInteger 只能保证单个变量的原子性,不能保护多个变量或代码块。
例如:原子地更新两个账户余额,必须用synchronized或分布式锁。
4.Unsafe的使用
AtomicInteger 底层依赖sun.misc.Unsafe,直接操作内存,JDK 9 后开始限制,未来可能被VarHandle替代。
六、面试模板话术
“AtomicInteger 是 Java 里基于 CAS 的无锁原子类,它属于乐观锁的范畴。核心是通过
Unsafe的compareAndSwapInt在一个自旋循环里比较并替换内存值,不需要线程阻塞,所以在高并发计数场景下性能远高于synchronized。但我清楚它的局限性:一是 ABA 问题,需要版本号解决;二是高竞争下自旋浪费 CPU,此时我用
LongAdder替代;三是它只能保护单个变量,不能保护复杂业务逻辑。在银行系统里,我通常用它做交易量统计、日切开关这类轻量、高频的操作,绝不会用它来保护资金余额的扣减——那是synchronized或分布式锁的职责。”
这样回答,既能讲清原理,又点出边界和替代方案,展现出你不仅会用,更知道何时该用、何时不该用的老练判断力。