跳到主要内容Java 并发编程艺术:原子操作类核心解析 | 极客日志Javajava算法
Java 并发编程艺术:原子操作类核心解析
Java 并发编程中的原子操作类涵盖基本类型、数组及引用类型的原子更新。核心机制基于 CAS(Compare-And-Swap)与自旋锁,通过 Unsafe 类调用 CPU 指令保证原子性。AtomicInteger 处理整型,AtomicIntegerArray 处理数组元素,AtomicReference 等处理对象引用。针对 CAS 的 ABA 问题,AtomicStampedReference 引入版本号机制,AtomicMarkableReference 使用标记位,有效区分引用版本变化,确保多线程环境下的数据一致性。
云朵棉花糖2 浏览 第 7 章 Java 中的原子操作类
7.1 原子更新基本类型类
这里说的 "3 个类",是指用于原子更新基本类型的核心类:
AtomicBoolean:原子更新布尔类型
AtomicInteger:原子更新整型
AtomicLong:原子更新长整型
核心方法解读
-
addAndGet(int delta)
- 原子性地将实例值与输入的
delta 相加,并返回更新后的结果。
- 例如:初始值为
5,调用 addAndGet(3) 后值变为 8,并返回 8。
-
compareAndSet(int expect, int update)
- 这是 CAS(Compare-And-Swap)机制的核心实现。
- 原理:如果当前值等于预期值
expect,则原子性地将值更新为 update,返回 true;否则不更新,返回 false。
-
getAndIncrement()
- 原子性地将当前值加 1,但返回的是自增前的旧值。
- 例如:初始值为
5,调用后值变为 6,但返回 5。
-
lazySet(int newValue)
- 最终会将值设置为
newValue,但不保证其他线程立即可见。
- 它是一种 "延迟" 的可见性保证,性能比
set() 更高,适合不需要立即让其他线程看到更新的场景。
这部分内容是《并发编程的艺术》对 AtomicInteger.getAndIncrement() 底层实现的深度解析:
核心实现原理:CAS + 自旋
getAndIncrement() 是典型的无锁编程实现,它的核心逻辑可以拆解为:
自旋循环
public final int getAndIncrement() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next)) return current;
}
}
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
这是一个 for (;;) 无限循环,也叫 "自旋",它会不断尝试直到 CAS 操作成功。
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
compareAndSwapInt 是 Unsafe 类提供的本地方法,直接调用 CPU 的 CAS 指令,保证了操作的原子性。
它会比较当前内存中的值是否等于预期值 expect,如果相等则更新为 update,并返回 true;否则返回 false。
- 第一步:获取当前值
current。
- 第二步:计算目标值
next = current + 1。
- 第三步:调用
compareAndSet 尝试原子更新。
- 如果成功,返回更新前的旧值
current。
- 如果失败(说明值被其他线程修改了),则回到循环开头,重新尝试。
这部分内容是《并发编程的艺术》中对 Unsafe 类核心 CAS 方法的解析:
Unsafe 类只暴露了三个底层 CAS 原子操作,它们都是本地方法(native),直接映射到 CPU 指令:
compareAndSwapObject(Object o, long offset, Object expected, Object x):用于原子更新对象引用类型的字段。
compareAndSwapInt(Object o, long offset, int expected, int x):用于原子更新 int 类型的字段。
compareAndSwapLong(Object o, long offset, long expected, long x):用于原子更新 long 类型字段。
这三个方法是整个 java.util.concurrent.atomic 包的基石。
因为 Unsafe 只支持 int、long 和 Object 三种类型的 CAS,所以其他基本类型都需要通过类型转换来间接实现原子更新:
AtomicBoolean:将 boolean 转成 int(true→1 false→0),再用 compareAndSwapInt 操作。
char/short/byte:转成 int 后,用 AtomicInteger 来实现原子更新。
float/double:通过 Float.floatToIntBits() 和 Double.doubleToRawLongBits() 转成 int/long,再用 AtomicInteger/AtomicLong 操作。
7.2 原子更新数组
它的设计目标是以原子方式更新数组里的整型元素,解决了普通数组在并发场景下修改元素时的线程安全问题。
addAndGet(int i, int delta):以原子方式,将数组中索引为 i 的元素与 delta 相加,并返回更新后结果。
compareAndSet(int i, int expect, int update):如果数组索引 i 处的当前值等于预期值 expect,则原子性地将其更新为 update,成功返回 true,失败返回 false。
7.3 原子更新引用类型
这部分内容是《并发编程的艺术》里关于原子更新引用类型的核心介绍:
AtomicInteger 这类原子类只能更新单个基本类型变量,而当你需要原子更新一个对象引用,或者对象里的多个字段时,就需要用到引用类型的原子类。
| 类名 | 核心作用 | 典型场景 |
|---|
| AtomicReference | 原子更新对象的引用本身 | 原子地将一个对象引用从旧值切换到新值,比如在无锁队列中更新头节点或尾节点。 |
| AtomicReferenceFieldUpdater | 原子更新对象引用类型的非静态字段 | 当你不能修改目标类的代码,又想原子更新它的某个引用字段时使用。 |
| AtomicMarkableReference | 原子更新对象引用 + 一个布尔标记位 | 解决 ABA 问题的一种方案,通过标记位来区分同一个引用的不同版本,比如在链表节点的原子操作中防止误判。 |
import java.util.concurrent.atomic.AtomicReference;
public class AtomicReferenceTest {
public static AtomicReference<User> atomicUserRef = new AtomicReference<>();
public static void main(String[] args) {
User user = new User("conan", 15);
atomicUserRef.set(user);
User updateUser = new User("Shinichi", 17);
boolean isSuccess = atomicUserRef.compareAndSet(user, updateUser);
System.out.println("CAS 更新是否成功:" + isSuccess);
System.out.println("当前用户名:" + atomicUserRef.get().getName());
System.out.println("当前年龄:" + atomicUserRef.get().getOld());
}
static class User {
private String name;
private int old;
public User(String name, int old) {
this.name = name;
this.old = old;
}
public String getName() {
return name;
}
public int getOld() {
return old;
}
}
}
这段代码完整展示了 AtomicReference 的核心使用流程,分为 4 个关键步骤:
AtomicReference<User> atomicUserRef = new AtomicReference<>();
AtomicReference 是泛型类,<User> 指定它要原子操作的是 User 类型的对象引用(不是对象本身,而是对象在内存中的地址)。
- 作用:把普通的对象引用包装成 "支持原子操作的引用",解决并发场景下对象引用修改的线程安全问题。
- 相当于给原子引用赋初始值,把
user 对象的引用存入 atomicUserRef 中。
- 类似
AtomicInteger 的 set() 方法,是普通的赋值操作(非原子 CAS)。
User updateUser = new User("Shinichi", 17);
- 创建一个新的
User 对象,作为要替换的目标引用。
atomicUserRef.compareAndSet(user, updateUser);
- 核心逻辑:
compareAndSet(预期引用,新引用) 是 CAS 机制的核心方法,原子性完成以下判断和操作:
- 检查
atomicUserRef 当前存储的引用是否等于 user(预期引用);
- 如果相等:将引用替换为
updateUser,返回 true;
- 如果不相等:不做任何修改,返回
false。
- 为什么需要这个操作?在并发场景下,普通的
= 赋值可能导致多线程覆盖,而 compareAndSet 能保证 "判断 + 赋值" 的原子性,不会被其他线程打断。
- 获取原子引用中当前存储的对象引用,进而访问对象的属性(
name/old)。
AtomicStampedReference 的原理
ABA 问题:在 CAS 操作中,一个值从 A 被修改为 B,又被改回 A,后续的 CAS 会误以为该值从未被修改过。
AtomicStampedReference 的 ABA 问题解决方案是为每个对象引用绑定一个整型版本号(Stamp)。
- 每次更新时,不仅更新对象引用,也会递增版本号。
- CAS 操作会同时检查引用值和版本号,只有两者都匹配时,才会执行更新。
- 这样即使引用值回到了旧值,只要版本号不同,就能识别出这是一个 "新的旧值",从而避免 ABA 问题。
public AtomicStampedReference(V initialRef, int initialStamp)
public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)
只有当当前引用等于 expectedReference,并且当前版本号等于 expectedStamp 时,才会原子性地将引用更新为 newReference,并将版本号更新为 newStamp。
public V getReference()
public int getStamp()
public void get(int[] stampHolder)
import java.util.concurrent.atomic.AtomicStampedReference;
public class AtomicStampedReferenceDemo {
public static void main(String[] args) {
AtomicStampedReference<String> asr = new AtomicStampedReference<>("A", 0);
new Thread(() -> {
int stamp = asr.getStamp();
String ref = asr.getReference();
System.out.println("线程 1 读取:引用=" + ref + ",版本号=" + stamp);
try {
Thread.sleep(100);
} catch (InterruptedException e) {}
boolean success = asr.compareAndSet("A", "C", stamp, stamp + 1);
System.out.println("线程 1 CAS 更新结果:" + success);
}).start();
new Thread(() -> {
try {
Thread.sleep(50);
} catch (InterruptedException e) {}
asr.compareAndSet("A", "B", asr.getStamp(), asr.getStamp() + 1);
System.out.println("线程 2 更新:A → B,版本号=" + asr.getStamp());
asr.compareAndSet("B", "A", asr.getStamp(), asr.getStamp() + 1);
System.out.println("线程 2 更新:B → A,版本号=" + asr.getStamp());
}).start();
}
}
线程 1 读取:引用=A,版本号=0
线程 2 更新:A → B,版本号=1
线程 2 更新:B → A,版本号=2
线程 1 CAS 更新结果:false
可以看到,虽然引用值从 A 变回了 A,但版本号已经从 0 变成了 2,所以线程 1 的 CAS 操作失败,成功避免了 ABA 问题。
对比 AtomicMarkableReference
AtomicStampedReference 使用整型版本号,可以精细地追踪每次更新。
AtomicMarkableReference 使用布尔标记位,只能区分 "是否被修改过",无法追踪修改次数。
- 因此,
AtomicStampedReference 更适合需要严格版本控制的场景,而 AtomicMarkableReference 适合只关心 "是否被修改过" 的简单场景。