前置总述
原子操作类是JUC(java.util.concurrent)提供的无锁化线程安全工具 ,包路径为java.util.concurrent.atomic,核心是基于CAS+Unsafe+volatile实现单个变量 的原子操作。
它的核心价值是替代传统的synchronized/Lock ,做到更轻量、无线程上下文切换开销,是高并发开发中处理单个变量线程安全的首选方案。
核心定位:细粒度、无锁、非阻塞的单个变量原子更新工具,无法替代锁解决多变量/代码块的原子性问题。
一、基础概念(面试开篇必问)
1. 原子操作的定义
原子操作 :指不可分割的操作 ,一个操作要么全部执行成功 ,要么全部不执行 ,不存在任何中间状态。
可以通俗理解为:操作是"一次性"的,不会被线程调度器打断,多线程下不会出现"操作做了一半"的情况。
2. 为什么需要原子操作类?------ 解决非原子操作的线程安全问题
Java中很多看似"简单"的操作其实是非原子的,最典型的就是**i++/count+=1**,这也是面试最常考的反例。
i++的本质:3个独立步骤(读→改→写)
- 读 :从主存中读取变量
i的当前值,加载到线程的工作内存; - 改 :在线程工作内存中对
i执行+1操作; - 写:将修改后的值写回主存。
多线程问题 :这3步是独立的,线程调度器可能在任意一步打断线程,导致中间值覆盖 ,最终计数丢失。
举个例子:线程A和B同时对i=1执行i++
- 线程A读
i=1,还没改,被打断; - 线程B读
i=1,完成+1,写回主存i=2; - 线程A恢复执行,基于之前读的
1+1成2,写回主存i=2; - 结果:两个线程各执行一次
i++,最终i=2而非预期的3,丢了一次计数。
原子操作类的核心作用,就是把这类"读-改-写"的非原子操作,封装成原子操作,避免多线程下的数值丢失。
3. Java实现原子操作的3种方式
Java中实现原子操作有3种核心方式,对应不同的并发场景,面试常考三者的区别:
| 实现方式 | 核心代表 | 核心思想(通俗说) | 特点 |
|---|---|---|---|
| 悲观锁 | synchronized、ReentrantLock | 认为一定会有线程竞争,先"占坑"(加锁),独占资源,其他线程阻塞等待 | 阻塞式、粗粒度,开销大(上下文切换) |
| 乐观锁 | atomic原子操作类 | 认为很少有线程竞争,不加锁,直接尝试更新,失败则重试 | 非阻塞式、细粒度,开销小(无上下文切换) |
| 底层CPU指令 | x86的cmpxchg指令 |
硬件层面直接保证操作的原子性,是前两者的底层支撑 | 最底层,JVM和Java类库基于此实现 |
核心关联:原子操作类的乐观锁(CAS),最终就是通过调用CPU的CAS硬件指令实现的,是"硬件层面的乐观锁"。
4. 原子操作类与锁(synchronized/Lock)的核心区别
面试必问的核心对比,一句话总结:原子类是无锁非阻塞的细粒度操作,锁是有锁阻塞的粗粒度操作,详细区别如下:
- 锁机制 :原子类无锁 (基于CAS自旋),锁有锁(独占锁,悲观锁);
- 线程状态 :原子类非阻塞 (线程失败时自旋重试,不进入阻塞状态),锁阻塞(线程失败时进入WAITING/BLOCKED,等待唤醒);
- 操作粒度 :原子类仅保证单个变量 的原子性,锁保证代码块/方法的原子性(多变量、多操作均可);
- 性能开销 :原子类无上下文切换/调度开销,轻量高效;锁存在上下文切换、唤醒等开销,重量;
- 适用场景 :原子类适用于单个变量的简单操作 (计数、状态标记),锁适用于多变量/复杂逻辑的原子性保证。
二、原子操作类的核心分类(按功能划分,核心使用)
原子操作类按操作数据类型+功能扩展 分为5大类,前4类JDK5新增,第5类是JDK8新增的高频核心类(大厂面试重点),其中标★的是面试最常考察、开发最常用的类。
整体分类速查表(建议熟记,面试开篇题常考):
| 分类 | 核心代表类 | 适用场景 | 核心注意点 |
|---|---|---|---|
| 基本类型原子类★ | AtomicInteger、AtomicLong、AtomicBoolean | 单个基本类型(int/long/boolean)原子更新 | AtomicBoolean底层用int实现(Unsafe仅支持数值CAS) |
| 数组类型原子类 | AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray | 数组中单个元素的原子更新 | 仅操作数组下标,非整个数组原子 |
| 引用类型原子类★ | AtomicReference、AtomicStampedReference★、AtomicMarkableReference★ | 单个对象引用的原子更新 | 专门解决CAS的ABA问题(面试核心) |
| 对象字段更新原子类★ | AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater | 普通对象的非静态字段原子更新 | 字段必须满足:volatile+非private+非static |
| JDK8原子累加器★ | LongAdder、DoubleAdder、LongAccumulator★、DoubleAccumulator★ | 高并发下的累加/计数场景 | 替代AtomicLong,解决高并发自旋瓶颈 |
子模块2.1 基本类型原子类(最基础、最常用)
核心是AtomicInteger、AtomicLong、AtomicBoolean,分别对应int、long、boolean类型的原子更新,三者用法高度一致,以AtomicInteger为核心学习即可,面试考察也以它为主。
核心特点
- 内部核心字段
value被volatile修饰(保证可见性和有序性); - 所有原子方法最终都通过
Unsafe的native方法实现(如compareAndSwapInt); - AtomicBoolean底层用int实现 (0表示false,1表示true),因为Unsafe类仅支持数值类型的CAS操作,无boolean类型的CAS方法(面试常考小细节)。
核心方法(AtomicInteger为例,熟记,开发/面试都常用)
方法名和功能一一对应,部分方法可以和普通的自增/累加操作类比,更好记:
| 方法名 | 功能说明 | 普通操作类比 | 面试注意点 |
|---|---|---|---|
| get() | 获取当前值 | - | volatile保证立即可见 |
| set(int newValue) | 设置新值 | - | 立即生效,volatile保证可见性 |
| getAndSet(int newValue) | 原子更新为新值,返回旧值 | - | 全原子操作 |
| getAndIncrement() | 原子执行i++,返回旧值 | i++ | 开发最常用的计数方法 |
| incrementAndGet() | 原子执行++i,返回新值 | ++i | 与上一个方法的区别是面试常考题 |
| getAndAdd(int delta) | 原子累加delta,返回旧值 | count += delta | 任意数值的原子累加 |
| addAndGet(int delta) | 原子累加delta,返回新值 | - | - |
| compareAndSet(int expect, int update) | 核心CAS方法:预期值==实际值则更新,返回是否成功 | - | 原子类的底层核心,所有原子方法都基于它实现 |
| lazySet(int newValue) | 延迟设置新值,不保证立即可见 | - | 性能更高,适用于无需严格可见性的场景 |
方法使用小示例
java
AtomicInteger ai = new AtomicInteger(0);
ai.incrementAndGet(); // 0→1,返回1(++i)
ai.getAndIncrement(); // 1→2,返回1(i++)
ai.getAndAdd(5); // 2→7,返回2
ai.compareAndSet(7, 10); // 预期7,更新为10,返回true
ai.lazySet(20); // 延迟设置为20,不保证立即可见
底层核心(面试常考)
以getAndIncrement()为例,它的底层实现是循环CAS(自旋锁),伪代码如下:
java
public final int getAndIncrement() {
// 循环调用CAS,直到更新成功(自旋)
while (true) {
int current = get(); // 获取当前值(volatile保证最新)
int next = current + 1; // 计算新值
// CAS核心:预期值current,更新为next,成功则返回current
if (compareAndSet(current, next)) {
return current;
}
}
}
核心思想 :失败就重试,直到CAS成功,这也是自旋锁的典型实现(后续会详细讲)。
子模块2.2 数组类型原子类
核心是AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray,对应数组的原子更新,开发中使用频率一般,面试考察较少,核心掌握"操作粒度"即可。
核心特点
- 仅保证数组中 单个元素的原子更新,而非整个数组的原子操作(面试易错点);
- 所有方法都基于数组下标操作,和普通数组的使用方式类似;
- 底层同样是CAS+Unsafe实现,数组的核心元素被volatile修饰。
核心使用示例
java
// 初始化数组,初始值[1,2,3]
AtomicIntegerArray arr = new AtomicIntegerArray(new int[]{1,2,3});
arr.getAndIncrement(0); // 下标0的元素原子+1,1→2,返回1
arr.compareAndSet(1, 2, 5); // 下标1,预期2,更新为5,返回true
arr.get(2); // 获取下标2的元素,值为3
核心注意 :如果直接用new int[]创建普通数组,多线程下修改数组元素会有线程安全问题,数组原子类就是解决这个问题的。
子模块2.3 引用类型原子类★(面试核心,解决ABA问题)
基本类型原子类操作的是"数值",而引用类型原子类操作的是对象的引用地址,核心解决两个问题:
- 单个对象引用的原子更新(如
User对象的引用); - CAS的天然缺陷------ABA问题(面试必问,大厂高频考点)。
该类包含3个核心类,其中AtomicStampedReference和AtomicMarkableReference是专门解决ABA问题的,是面试考察的重点。
2.3.1 基础类:AtomicReference
操作对象引用 的原子更新,用法和AtomicInteger高度一致,只是把"数值"换成了"对象引用"。
适用场景:多线程下原子更新一个对象的引用,比如多线程下修改一个配置对象的引用。
核心使用示例
java
// 定义一个普通对象
class User {
private String name;
private int age;
// 构造器、get/set省略
}
public class AtomicReferenceDemo {
public static void main(String[] args) {
// 初始化AtomicReference,引用一个User对象
AtomicReference<User> ar = new AtomicReference<>(new User("张三", 20));
// 获取当前引用的对象
User oldUser = ar.get();
// 新建一个User对象
User newUser = new User("李四", 25);
// CAS原子更新引用:预期oldUser,更新为newUser
boolean success = ar.compareAndSet(oldUser, newUser);
System.out.println(success); // true
System.out.println(ar.get().getName()); // 李四
}
}
2.3.2 CAS的致命缺陷:ABA问题(面试必问,核心中的核心)
这是引用类型原子类的核心考点,必须理解定义、危害、解决方案,缺一不可。
1. ABA问题的通俗定义
线程1准备对变量V执行CAS操作,预期值为A ;在线程1执行CAS之前,线程2先将V从A→B→A(先改B再改回A);线程1执行CAS时,发现V的值还是A,误以为V从未被修改过,于是成功执行CAS,导致数据一致性问题。
可以用一个生活例子理解:
你把100块钱放在桌子上,出去接电话,同桌先把100块拿走,又放回来100块,你回来后看到桌子上还是100块,以为没人动过,但其实钱已经被拿走过了------这就是典型的ABA问题。
2. ABA问题的危害
简单计数场景 :ABA问题无实际危害 ,比如只是简单的i++,即使出现ABA,最终的数值结果还是正确的;
带状态的对象/复杂结构场景 :ABA问题会导致逻辑错误 ,这是最危险的,比如链表的原子入队/出队操作 、对象的状态更新。
链表场景的ABA问题示例 (大厂面试常考):
链表节点:A→B→C,线程1准备将A节点出队(CAS将头节点从A改为B);线程2先将A出队,再将A重新入队,链表变回A→B→C;线程1执行CAS时,头节点还是A,误以为没动,成功执行CAS,但此时链表的内部状态已经被修改,可能导致链表遍历异常、节点丢失等问题。
3. ABA问题的核心解决方案
为变量绑定一个「版本号/标记位」 ,让CAS操作从仅对比"值" 变为同时对比"值+版本号/标记位" 。
只要变量被修改过,不管最终值是否变回原值,版本号/标记位都会发生变化,CAS时因为版本号不匹配,就会执行失败,从而解决ABA问题。
2.3.3 解决ABA问题的两个核心类(面试必背区别)
基于"值+版本号/标记位"的解决方案,JUC提供了两个类,分别对应不同的场景,面试常考两者的区别和适用场景。
1. AtomicStampedReference★(全量解决ABA问题,大厂面试重点)
核心 :为变量绑定一个int类型的版本戳(stamp) ,每次更新变量时,版本戳必须+1 ,CAS时需要同时对比「对象引用+版本戳」,两者都匹配才能更新成功。
适用场景 :需要记录变量修改次数 的场景,比如链表、带状态的对象更新,是解决ABA问题的首选。
核心方法(面试必记):
java
// 构造器:初始值 + 初始版本戳
AtomicStampedReference(V initialRef, int initialStamp)
// 获取当前的「值+版本戳」,通过数组返回(Java无法返回多个值)
get(int[] stampHolder)
// 核心CAS方法:预期值、新值、预期版本戳、新版本戳,全匹配才更新
compareAndSet(V expectedRef, V newRef, int expectedStamp, int newStamp)
// 获取当前版本戳
int getStamp()
// 获取当前值
V getReference()
手写演示ABA问题并解决 (面试手写题高频,必须会写):
见本文末尾的「核心手写题示例2」,已附带详细注释和输出结果。
2. AtomicMarkableReference(半解决ABA问题,面试考区别)
核心 :为变量绑定一个boolean类型的标记位(mark) ,仅表示变量**"是否被修改过"(true=被修改过,false=未被修改过),CAS时同时对比「对象引用+标记位」。
适用场景:只需判断变量是否被修改过**,无需记录修改次数的场景,比如简单的对象状态判断。
核心区别(面试必背):
- AtomicStampedReference:版本戳是int类型,可递增 ,能记录变量的修改次数 ,全量解决ABA问题;
- AtomicMarkableReference:标记位是boolean类型,仅true/false ,只能判断是否被修改过 ,半解决ABA问题(若变量被修改多次,标记位可能变回原值)。
子模块2.4 对象字段更新原子类★(灵活更新,面试考使用条件)
核心是AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater,简称字段更新器。
核心作用(为什么需要?)
在开发中,如果你想让一个普通对象的某个字段 支持原子更新,不需要为这个字段专门创建一个原子类(比如为User的age字段创建AtomicInteger),而是可以通过字段更新器 ,基于反射 实现普通对象字段的原子更新,节省内存空间,让代码更灵活。
简单说:用原子方式更新普通对象的非静态字段,无需修改原对象的代码。
强制使用条件★(面试必问,高频易错点)
字段更新器有4个强制使用条件 ,只要违反其中一个,就会抛出异常(如IllegalArgumentException),必须熟记并理解每个条件的原因:
- 目标字段必须被
volatile修饰:保证字段的可见性和有序性,否则CAS读取的是旧值,原子更新无意义(面试核心原因); - 目标字段不能是
private:字段更新器基于反射实现,反射无法获取私有的字段; - 目标字段不能是
static:字段更新器是基于对象实例的,静态字段属于类,而非单个实例,无法通过实例更新; - 必须通过
newUpdater()方法创建更新器:不能直接new,该方法通过泛型保证类型安全,参数为「对象的Class」+「字段名」。
核心使用示例
java
// 普通对象:age字段满足volatile+非private+非static
class User {
public volatile String name;
public volatile int age; // 核心字段,满足所有条件
public User(String name, int age) {
this.name = name;
this.age = age;
}
}
public class FieldUpdaterDemo {
public static void main(String[] args) {
// 1. 创建字段更新器:User类的age字段(int类型)
AtomicIntegerFieldUpdater<User> ageUpdater = AtomicIntegerFieldUpdater.newUpdater(User.class, "age");
// 2. 创建普通User对象
User user = new User("张三", 20);
// 3. 原子更新age字段:+1
ageUpdater.getAndIncrement(user);
System.out.println(user.age); // 21
// 4. CAS更新age字段:预期21,更新为25
boolean success = ageUpdater.compareAndSet(user, 21, 25);
System.out.println(success); // true
System.out.println(user.age); // 25
}
}
子模块2.5 JDK8原子累加器★(高并发核心,大厂面试高频对比)
核心是LongAdder、DoubleAdder、LongAccumulator、DoubleAccumulator,是JDK8为解决高并发计数瓶颈新增的核心类 ,面试常考与AtomicLong的对比,开发中高并发计数场景几乎全用它替代AtomicLong。
2.5.1 为什么需要原子累加器?------ AtomicLong的高并发瓶颈
AtomicLong的底层是单变量CAS自旋 ,在低并发场景 下,自旋次数少,性能很高;但在高并发场景 下,大量线程同时竞争同一个变量的CAS,导致大部分线程都在自旋重试 ,CPU占用率飙升,性能急剧下降------这就是AtomicLong的自旋瓶颈。
用生活例子理解:
AtomicLong相当于一个窗口打饭 ,所有同学都排队挤这个窗口,人多的时候排队时间极长;而LongAdder相当于多个窗口打饭,同学分散到不同窗口,排队时间大幅缩短,效率大幅提升。
2.5.2 核心类1:LongAdder/DoubleAdder(高并发累加首选)
LongAdder对应long类型,DoubleAdder对应double类型,以LongAdder为核心学习 ,是开发中高并发计数/累加的首选(如接口访问量、订单数统计)。
核心原理:分段CAS(Striped64)
LongAdder的底层抛弃了"单变量CAS",改为分段CAS(Striped64算法),内部维护两个核心变量:
- base变量:低并发场景下,直接CAS更新base变量(和AtomicLong一致),避免数组开销;
- cell数组 :高并发场景下,将竞争分散到cell数组的不同元素 ,每个线程通过哈希算法对应一个cell元素,线程只CAS更新自己的cell元素,避免单变量竞争。
最终计数结果 :sum() = base + 所有cell数组元素的值,通过汇总base和cell数组得到最终结果。
核心方法
java
add(long x) // 原子累加x(最常用,如add(1)就是计数+1)
increment() // 原子+1,等价于add(1)
decrement() // 原子-1,等价于add(-1)
sum() // 汇总base+cell,返回最终计数
reset() // 重置base和cell为0,用于重复计数
sumThenReset() // 先sum()再reset(),原子操作
核心注意点★(面试必问,高频易错点)
- sum()方法是最终一致性,非实时一致性 :汇总base+cell时,没有加锁,可能有线程正在修改cell数组,导致sum()的结果不是"实时最新值",存在微小延迟(面试核心原因);
- cell数组是懒加载的:初始时cell数组为空,只有当高并发竞争base变量时,才会初始化cell数组(节省内存);
- DoubleAdder底层用long存储 :因为Unsafe类不支持double类型的CAS操作,DoubleAdder通过
Double.doubleToLongBits()将double转为long,CAS更新后再通过Double.longBitsToDouble()转回double(面试小细节); - 适用场景 :高并发下的统计/计数场景 (如接口访问量、页面点击量),无需实时精确计数的场景;金融场景(如交易金额统计)禁止使用,因为需要实时精确计数。
2.5.3 核心类2:LongAccumulator/DoubleAccumulator(自定义累加规则)
是LongAdder/DoubleAdder的超集 ,不仅支持简单的"加法",还可以自定义累加规则(如乘法、求最大值、最小值),底层原理和LongAdder一致,也是分段CAS。
核心特点
通过**BinaryOperator函数式接口**自定义累加规则,构造器需要传入两个参数:
- 累加规则(BinaryOperator);
- 初始值。
核心使用示例(高并发求最大值,面试常考)
java
import java.util.concurrent.atomic.LongAccumulator;
import java.util.function.BinaryOperator;
public class LongAccumulatorDemo {
public static void main(String[] args) {
// 自定义累加规则:求两个数的最大值,初始值0
BinaryOperator<Long> maxFunc = (x, y) -> Math.max(x, y);
LongAccumulator la = new LongAccumulator(maxFunc, 0L);
// 多线程下更新,求最大值
new Thread(() -> la.accumulate(10)).start();
new Thread(() -> la.accumulate(20)).start();
new Thread(() -> la.accumulate(15)).start();
// 休眠等待线程执行完成
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(la.get()); // 20(高并发下原子求最大值)
}
}
2.5.4 LongAdder vs AtomicLong★(面试必背对比,大厂高频)
这是JDK8原子类的核心面试题 ,必须熟记适用场景 和性能差异原因:
| 特性 | AtomicLong | LongAdder |
|---|---|---|
| 底层原理 | 单变量CAS自旋 | 分段CAS(base+cell数组) |
| 高并发性能 | 差(自旋瓶颈,CPU占用高) | 好(分散竞争,自旋次数少) |
| 计数一致性 | 实时一致性(CAS单变量,精确) | 最终一致性(sum()汇总无锁,有延迟) |
| 方法支持 | 丰富(getAndAdd、compareAndSet等) | 简单(add、increment、sum等) |
| 内存开销 | 小(仅一个volatile long) | 稍大(base+cell数组,懒加载) |
| 适用场景★ | 1. 低并发计数;2. 需实时精确计数的场景(如金融);3. 需要CAS高级方法的场景 | 1. 高并发统计/计数场景(如接口访问量);2. 无需实时精确计数的场景;3. 简单累加场景 |
| 性能优势★ | 低并发下略优于LongAdder(无数组开销) | 高并发下性能是AtomicLong的10倍以上 |
三、原子操作类的核心实现原理★(面试重中之重,底层必问)
原子操作类的底层是CAS + Unsafe + volatile 的组合,三者缺一不可 ,共同支撑原子操作类的线程安全,这是大厂面试必问的底层原理,必须理解三者的作用和关联关系。
一句话总结三者的核心分工:
volatile保证可见性和有序性(让线程能看到最新值),CAS保证原子性(让线程改值时不冲突),Unsafe提供底层实现(直接操作内存和CPU指令)。
子模块3.1 CAS(Compare and Swap,比较并交换)★
CAS是乐观锁的核心算法 ,也是原子操作类的底层核心 ,所有原子操作最终都通过CAS实现,面试必问定义、执行流程、优缺点。
1. CAS的通俗定义
CAS包含3个操作数,是一个"原子的比较并交换"操作:
- V :要更新的变量的内存地址(通过Unsafe获取);
- A :线程读取到的变量的旧的预期值;
- B :线程计算出的变量的新值。
执行逻辑 (必须熟记):
当且仅当内存地址V中的实际值 == 预期值A时 ,将V中的值原子更新 为B;否则不做任何操作 ,返回false。线程可以选择自旋重试 (循环CAS)或放弃更新。
2. CAS的执行流程(从Java到硬件,层层拆解)
CAS的执行是从上层Java到底层硬件的层层调用,面试常考这个流程:
- Java层 :原子操作类调用
Unsafe的CAS方法(如compareAndSwapInt); - JVM层 :Unsafe的CAS方法是native方法,JVM将其解析为底层的C/C++代码;
- 硬件层 :C/C++代码调用CPU的CAS硬件指令 (如x86架构的
cmpxchg指令),这是CAS的最终实现。
3. 多核CPU下的CAS原子性保证(面试常考)
CAS指令在单核CPU 下天然原子(无多核竞争,指令执行不会被打断);但在多核CPU 下,需要保证多个CPU不会同时修改同一个内存地址,解决方案是:
为CAS指令添加lock前缀 ,让CPU执行该指令时独占系统总线,禁止其他CPU修改该内存地址,从而保证多核CPU下CAS操作的原子性。
4. CAS的优缺点★(面试必背)
优点(乐观锁的核心优势)
- 无锁、非阻塞:无需加锁,线程失败时自旋重试,不进入阻塞状态;
- 无上下文切换开销:避免了锁的上下文切换、线程唤醒等开销,轻量高效;
- 粒度细:仅针对单个变量的原子操作,比锁的粗粒度操作更灵活。
缺点(面试必问,也是原子操作类的局限性)
- ABA问题:CAS的天然缺陷,已通过AtomicStampedReference解决;
- 自旋开销大:高并发下大量线程自旋重试,导致CPU空转,占用率飙升;
- 只能保证单个变量的原子性 :CAS仅能对单个变量实现原子操作,无法对多个变量、代码块实现原子性(若需要,需将多个变量封装成一个对象,用AtomicReference操作);
- 无法解决公平性问题:CAS是随机重试,无法保证线程的执行顺序,可能导致某些线程一直自旋失败。
子模块3.2 Unsafe类(Java的底层工具类,原子操作类的基石)
sun.misc.Unsafe是Java提供的底层本地方法类 ,可以理解为Java的"后门",直接操作内存、CPU指令、对象 ,是原子操作类的底层实现基石。
1. Unsafe的核心作用(面试常考)
原子操作类的所有核心功能,最终都通过Unsafe实现,主要包括:
- 实现CAS操作 :提供
compareAndSwapInt、compareAndSwapLong、compareAndSwapObject等native方法,是CAS的直接实现; - 获取变量的内存偏移量 :通过
objectFieldOffset方法获取对象字段的内存偏移量 ,后续CAS通过偏移量直接操作内存,无需通过对象的get/set方法,保证效率; - 直接操作内存:可以直接读取、修改对象的内存值,绕过JVM的安全检查;
- 线程操作 :提供线程的挂起(
park)、唤醒(unpark)方法,是LockSupport的底层实现。
2. Unsafe的关键注意点★(面试常考)
- 不能直接实例化 :Unsafe的构造方法是
private的,无法通过new Unsafe()创建实例,必须通过反射获取; - native方法:Unsafe的所有核心方法都是native的,直接与操作系统/CPU交互,跳过JVM的安全检查;
- 使用风险高 :直接操作内存和CPU,使用不当会导致内存泄漏、JVM崩溃、数据错乱,因此JDK对Unsafe的使用做了限制,普通开发中不建议直接使用;
- 原子操作类的专属工具:Unsafe是JUC原子操作类、锁框架的核心依赖,是Java并发的底层基础。
3. Unsafe的简单使用(了解即可,面试不要求手写)
java
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class UnsafeDemo {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
// 1. 通过反射获取Unsafe的theUnsafe字段
Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafeField.setAccessible(true); // 取消访问权限检查
// 2. 获取Unsafe实例
Unsafe unsafe = (Unsafe) theUnsafeField.get(null);
// 3. 示例:获取AtomicInteger的value字段的内存偏移量
Field valueField = AtomicInteger.class.getDeclaredField("value");
long valueOffset = unsafe.objectFieldOffset(valueField);
System.out.println(valueOffset); // 输出value字段的内存偏移量
}
}
子模块3.3 volatile关键字(保证可见性和有序性)★
volatile是Java的轻量级同步关键字,原子操作类的所有核心字段 (如AtomicInteger的value、LongAdder的base)都被volatile修饰,这是保证原子操作类线程安全的重要一环。
1. volatile的核心作用(面试必记,仅两个)
volatile仅保证可见性和有序性,不保证原子性(这是面试高频易错点),这两个作用正是原子操作类需要的:
(1)保证可见性
一个线程修改了volatile修饰的变量,其他线程能立即看到最新值 ,避免了多线程下的缓存一致性问题 。
原理:修改volatile变量时,JVM会强制将该变量的最新值从线程的工作内存刷新到主存,同时让其他线程的工作内存中该变量的缓存失效,其他线程必须从主存重新读取。
(2)保证有序性
禁止JVM对volatile修饰的变量进行指令重排 ,保证代码的执行顺序和编写顺序一致,避免线程读取到变量的中间值 (如i++的读-改-写步骤被重排)。
原理 :通过内存屏障实现,在volatile变量的读、写操作前后插入内存屏障,禁止指令重排。
2. 高频易错点★
volatile不保证原子性 :比如普通的volatile int i = 0;,多线程下执行i++仍然会有线程安全问题,因为i++是读-改-写三步,volatile只能保证每一步的可见性,无法保证三步的原子性。
原子操作类的解决方式 :volatile + CAS,volatile保证可见性和有序性,CAS保证读-改-写三步的原子性,两者结合才实现了完整的线程安全。
子模块3.4 CAS + Unsafe + volatile 三者的关联关系★(面试必背)
这是原子操作类底层原理的核心考点,用一句话总结:
volatile保证线程能看到变量的最新值(可见性),避免CAS读取到旧值;CAS保证变量的更新操作是原子的,避免多线程下的中间值覆盖;Unsafe提供底层实现,通过获取内存偏移量直接操作内存,调用CPU的CAS指令,是前两者的基础。
用流程图简化理解:
线程读取变量 → volatile保证读取的是主存最新值 → 计算新值 → Unsafe通过内存偏移量执行CAS → CAS保证原子更新 → 成功则更新主存,失败则自旋重试
四、自旋锁(原子操作类的底层锁思想)
原子操作类的CAS实现,本质就是自旋锁 的典型应用,面试常考自旋锁的定义、思想、适用场景,以及与阻塞锁的对比。
1. 自旋锁的通俗定义
自旋锁是非阻塞锁 ,线程尝试获取锁失败时,不进入阻塞状态 (WAITING/BLOCKED),而是循环自旋(不断尝试CAS操作),直到获取锁成功为止。
2. 原子操作类中的自旋锁实现
以AtomicInteger的getAndIncrement()为例,它的底层就是循环CAS(自旋),伪代码如下:
java
public final int getAndIncrement() {
while (true) { // 自旋:失败就循环
int current = get(); // 获取最新值
int next = current + 1;
if (compareAndSet(current, next)) { // CAS尝试更新
return current; // 成功则返回,退出自旋
}
// 失败则继续循环,自旋重试
}
}
核心思想:失败就重试,直到成功,不放弃CPU执行权。
3. 自旋锁的适用场景★(面试必问)
自旋锁的核心优势是无上下文切换开销 ,但自旋会占用CPU资源,因此仅适用于:
轻量级竞争、操作耗时极短的场景(如简单的计数、状态标记),此时自旋的开销远小于阻塞锁的上下文切换开销。
禁止使用场景:竞争激烈、操作耗时久的场景,此时大量线程自旋会导致CPU空转,占用率飙升,性能远低于阻塞锁。
4. 自旋锁 vs 阻塞锁(synchronized/Lock)★(面试必背)
| 特性 | 自旋锁(原子类/CAS) | 阻塞锁(synchronized/ReentrantLock) |
|---|---|---|
| 锁类型 | 非阻塞锁、乐观锁 | 阻塞锁、悲观锁 |
| 线程状态 | 运行中(RUNNABLE),自旋重试 | 阻塞状态(WAITING/BLOCKED),等待唤醒 |
| 核心开销 | CPU自旋开销 | 上下文切换、线程唤醒开销 |
| 适用场景 | 轻量竞争、操作耗时极短 | 重度竞争、操作耗时久 |
| 实现原理 | CAS循环重试 | 锁队列+阻塞/唤醒 |
| 性能 | 低并发下极高,高并发下极低 | 低并发下略低,高并发下稳定 |
五、原子操作类的使用场景★(面试常问「什么时候用?」)
原子操作类是无锁化的线程安全工具,适用于单个变量的简单操作 ,开发中常见的使用场景如下,同时标注避坑点,避免误用。
1. 核心适用场景
- 简单计数/统计:接口访问量、页面点击量、订单数、计数器(低并发用AtomicInteger/AtomicLong,高并发用LongAdder);
- 状态标记:开关状态(如服务的启动/停止,AtomicBoolean)、对象的状态更新(如订单的待支付/已支付,AtomicReference);
- 无锁化的对象字段更新:普通对象的非静态字段原子更新(如User的age字段,AtomicIntegerFieldUpdater),无需修改原对象代码;
- 解决ABA问题的场景:链表的原子入队/出队、带状态的对象更新(AtomicStampedReference);
- 自定义累加规则:高并发下求最大值、最小值、乘法(LongAccumulator);
- 单对象引用的原子更新:多线程下修改配置对象、核心对象的引用(AtomicReference)。
2. 高频避坑点★(面试/开发都要记)
这是原子操作类最容易误用的点,也是面试常考的易错点:
原子操作类仅适用于 单个变量的原子操作,若需要保证 多个变量或 代码块的原子性,必须使用synchronized或ReentrantLock,原子操作类无法解决这类问题。
反例 :多线程下同时更新x和y两个变量,即使x和y都是AtomicInteger,也无法保证两个变量的更新是原子的,此时需要用锁包裹两个更新操作。
正例 :若要实现多个变量的原子更新,可将多个变量封装成一个对象,用AtomicReference操作该对象的引用,通过CAS原子更新整个对象(面试常考的解决方案)。
六、高频面试要点汇总(分题型,必背)
结合大厂面试考察重点,将原子操作类的面试题分为基础概念题、进阶原理题、手写题、场景分析题、易错点 ,标★的是必背题,直接考的概率极高。
题型1:基础概念题(面试开篇,必背)
- ★ 什么是原子操作?为什么
i++不是原子操作? - ★ Java中实现原子操作的方式有哪些?原子操作类与synchronized的区别是什么?
- ★ 原子操作类的包路径是什么?主要分为哪几类?
- ★ AtomicInteger的
getAndIncrement()和incrementAndGet()的区别是什么? - 为什么AtomicBoolean底层是用int实现的?
- 数组类型原子类的操作粒度是什么?是否保证整个数组的原子性?
题型2:进阶原理题(面试核心,考察深度,必背)
- ★ 原子操作类的底层实现原理是什么?CAS、Unsafe、volatile分别起什么作用?
- ★ 什么是CAS?CAS的执行流程、优缺点分别是什么?
- ★ 什么是ABA问题?产生原因是什么?如何解决?AtomicStampedReference和AtomicMarkableReference的区别是什么?
- ★ JDK8为什么新增LongAdder?LongAdder比AtomicLong高效的原因是什么?底层原理是什么?
- ★ LongAdder的
sum()方法为什么是最终一致性,而非实时一致性?适用场景是什么? - ★ 对象字段更新原子类(AtomicXXXFieldUpdater)的使用条件有哪些?为什么要求字段是volatile的?
- ★ LongAdder和AtomicLong的区别是什么?分别适用于什么场景?
- Unsafe类的作用是什么?为什么不能直接实例化?
- 自旋锁的思想是什么?原子操作类如何体现自旋锁?自旋锁的适用场景和缺点是什么?
- CAS的底层是如何实现的?多核CPU下如何保证CAS的原子性?
题型3:手写编程题(考察实操,高频)
- ★ 手写一个基于AtomicInteger的线程安全计数器,实现incr()和getCount()方法;
- ★ 手写代码演示CAS的ABA问题,并使用AtomicStampedReference解决;
- 手写代码使用AtomicIntegerFieldUpdater更新普通对象的字段;
- 手写代码对比AtomicLong和LongAdder在高并发下的性能差异(用CountDownLatch模拟高并发);
- ★ 手写代码使用LongAccumulator实现高并发下的最大值/最小值统计;
- 手写代码用AtomicReference实现多个变量的原子更新(封装成对象)。
题型4:场景分析题(考察应用,大厂常考)
- ★ 高并发下统计接口的访问量,用AtomicLong还是LongAdder?为什么?
- ★ 金融场景中统计交易金额,用AtomicLong还是LongAdder?为什么?
- 链表的原子入队操作,需要解决什么问题?用哪个原子类?
- 如何实现多个变量的原子更新?(如同时更新x和y)
- 什么时候用AtomicStampedReference?什么时候用AtomicMarkableReference?
- 低并发下计数,用AtomicLong还是LongAdder?为什么?
题型5:高频易错点(面试避坑,必记)
- 误以为原子操作类能解决多个变量的原子性问题(实际仅支持单个变量);
- 误以为LongAdder的
sum()方法是实时一致的(实际是最终一致,高并发下有延迟); - 使用字段更新器时,字段未加volatile/设为private/static(导致创建失败/非原子);
- 误以为CAS一定比锁高效(高并发/操作耗时久时,CAS自旋开销>锁的上下文切换开销);
- 混淆AtomicStampedReference和AtomicMarkableReference的使用场景(版本号vs标记位);
- 误以为AtomicInteger的
set()方法需要CAS(实际set是直接赋值,volatile保证可见性,无需CAS); - 忘记LongAdder的cell数组是懒加载的(初始为空,高并发竞争时才初始化);
- 金融场景中误用LongAdder(需要实时精确计数,应使用AtomicLong);
- 误以为volatile保证原子性(实际仅保证可见性和有序性,需配合CAS)。
七、核心手写题示例(必写,面试直接考)
以下两个手写题是大厂面试高频直接考的题目,必须会写、会解释、会运行,附带详细注释和输出结果。
示例1:基于AtomicInteger的线程安全计数器
要求:实现incr()(原子+1)和getCount()(获取当前值)方法,测试10个线程各计数1000次,保证结果正确。
java
import java.util.concurrent.atomic.AtomicInteger;
/**
* 基于AtomicInteger的线程安全计数器(面试手写题必写)
*/
public class AtomicCounter {
// 核心原子变量,AtomicInteger内部的value已被volatile修饰
private final AtomicInteger count = new AtomicInteger(0);
// 原子+1方法(开发最常用)
public void incr() {
count.incrementAndGet(); // 原子++i,返回新值
// 若需要返回旧值,用count.getAndIncrement()
}
// 获取当前计数,volatile保证可见性
public int getCount() {
return count.get();
}
// 测试:10个线程,每个线程计数1000次
public static void main(String[] args) throws InterruptedException {
AtomicCounter counter = new AtomicCounter();
int threadNum = 10; // 线程数
int times = 1000; // 每个线程计数次数
Thread[] threads = new Thread[threadNum];
// 创建并启动线程
for (int i = 0; i < threadNum; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < times; j++) {
counter.incr();
}
});
threads[i].start();
}
// 等待所有线程执行完成
for (Thread t : threads) {
t.join();
}
// 输出结果:10000(线程安全,无计数丢失)
System.out.println("最终计数结果:" + counter.getCount());
}
}
输出结果 :最终计数结果:10000(线程安全,无丢失)。
示例2:演示ABA问题并使用AtomicStampedReference解决
要求:用两个线程演示ABA问题,线程1准备将1→2,线程2执行1→3→1的ABA操作,使用AtomicStampedReference让线程1的CAS失败,成功解决ABA问题。
java
import java.util.concurrent.atomic.AtomicStampedReference;
/**
* 演示CAS的ABA问题,并使用AtomicStampedReference解决(面试手写题必写)
*/
public class ABADemo {
// 初始化:值为1,初始版本戳为1
private static final AtomicStampedReference<Integer> asr = new AtomicStampedReference<>(1, 1);
public static void main(String[] args) throws InterruptedException {
// 线程1:准备将值从1→2,预期版本戳1
Thread t1 = new Thread(() -> {
// 获取当前值和版本戳
int stamp = asr.getStamp();
Integer ref = asr.getReference();
System.out.println("线程1-获取值:" + ref + ",版本戳:" + stamp);
try {
Thread.sleep(1000); // 休眠1秒,让线程2执行ABA操作
} catch (InterruptedException e) {
e.printStackTrace();
}
// 尝试CAS:值1→2,版本戳1→2(全匹配才更新)
boolean success = asr.compareAndSet(1, 2, stamp, stamp + 1);
System.out.println("线程1-CAS结果:" + success + ",当前值:" + asr.getReference() + ",当前版本戳:" + asr.getStamp());
});
// 线程2:执行ABA操作:1→3→1,版本戳1→2→3
Thread t2 = new Thread(() -> {
int stamp = asr.getStamp();
Integer ref = asr.getReference();
System.out.println("线程2-获取值:" + ref + ",版本戳:" + stamp);
// 第一步:1→3,版本戳1→2
asr.compareAndSet(1, 3, stamp, stamp + 1);
System.out.println("线程2-将1→3,当前版本戳:" + asr.getStamp());
// 第二步:3→1,版本戳2→3(先获取最新版本戳)
stamp = asr.getStamp();
asr.compareAndSet(3, 1, stamp, stamp + 1);
System.out.println("线程2-将3→1,当前版本戳:" + asr.getStamp());
});
// 启动线程
t1.start();
t2.start();
// 等待线程执行完成
t1.join();
t2.join();
}
}
输出结果:
线程1-获取值:1,版本戳:1
线程2-获取值:1,版本戳:1
线程2-将1→3,当前版本戳:2
线程2-将3→1,当前版本戳:3
线程1-CAS结果:false,当前值:1,当前版本戳:3
核心说明 :线程1的CAS因版本戳不匹配(预期1,实际3)执行失败,即使值变回了1,也成功解决了ABA问题。
- 原子操作类是无锁化 的线程安全工具,基于
CAS+Unsafe+volatile实现,适用于单个变量的原子更新; - 原子操作类分为5大类,其中基本类型、引用类型、字段更新器、JDK8累加器是面试重点,LongAdder是高并发计数的首选;
- CAS是原子操作类的底层核心,存在ABA问题,通过AtomicStampedReference(版本戳) 全量解决;
- JDK8的LongAdder通过分段CAS(base+cell数组) 解决AtomicLong的高并发自旋瓶颈,sum()是最终一致性,适用于非金融的统计场景;
- 字段更新器基于反射实现,要求字段volatile+非private+非static;
- CAS+Unsafe+volatile三者缺一不可:volatile保证可见性,CAS保证原子性,Unsafe提供底层实现;
- 原子操作类的CAS是自旋锁的典型实现,适用于轻量竞争、操作耗时极短的场景;
- 原子操作类仅支持单个变量的原子操作,多变量/代码块的原子性需要用synchronized/Lock;
- LongAdder vs AtomicLong:低并发用AtomicLong(精确),高并发非金融场景用LongAdder(高效),金融场景必须用AtomicLong(实时精确)。