JUC从实战到源码:CAS原理与机制详解
😄生命不息,写作不止
🔥 继续踏上学习之路,学之分享笔记
👊 总有一天我也能像各位大佬一样
🏆 博客首页 @怒放吧德德 To记录领地 @一个有梦有戏的人
🌝分享学习心得,欢迎指正,大家一起学习成长!
转发请携带作者信息 @怒放吧德德(掘金) @一个有梦有戏的人(CSDN)
前言
在多线程编程中,线程安全一直是核心问题之一。传统的同步机制,如 synchronized,虽然能够保证线程安全,但在高并发场景下往往会导致线程阻塞,从而降低系统性能。随着并发编程的发展,无锁编程(lock-free programming)逐渐成为一种更高效、更优雅的解决方案。而 CAS(Compare-and-Swap,比较并交换)机制正是无锁编程的核心思想之一。
在 Java 的并发编程中,CAS 机制被广泛应用,尤其是在 java.util.concurrent.atomic
包中的原子类(如 AtomicInteger、AtomicLong 等)中。这些原子类通过硬件级别的原子操作,实现了高效的线程安全操作,避免了传统锁带来的性能瓶颈。因此,深入理解 CAS 的原理及其在 Java 中的实现,对于掌握并发编程至关重要。
本文将从 CAS 的基本原理出发,结合 Java 中的原子类,详细探讨其工作原理、优缺点以及相关的应用场景。同时,我们还会通过代码示例和源码分析,帮助读者更好地理解 CAS 在实际开发中的应用。
CAS 原理
CAS(Compare-and-Swap,比较并交换)是一种在多线程编程中常用的同步机制,用于实现无锁编程(lock-free programming)。它在Java的JUC(Java Util Concurrency)包中被广泛应用,尤其是在 java.util.concurrent.atomic 包中的原子类(如 AtomicInteger 、 AtomicLong 等)中,他们都是 CAS 的落地实现。其原子操作的本质就是硬件指令的支持(如CPU的cmpxchg
指令)。
简单例子熟悉
假如我们需要执行 i++操作,在多线程下,不使用原子类来保证线程安全。我们看以下例子,通过 synchronized 可以达到线程安全,但是我们知道,synchronized 是重量级锁,这并不是很好的解决方式。
java
public class NoUseCas {
public volatile Integer number;
public synchronized void numberIncrease(Integer number) {
number++;
}
public Integer getNumber() {
return number;
}
}
接下来我们使用原子类来操作 i++。
java
public class AtoCase {
AtomicInteger atomicInteger = new AtomicInteger();
public int getAtomicInteger() {
// get方法能够获取值
return atomicInteger.get();
}
public void setAtomicInteger() {
// 执行i++操作
atomicInteger.getAndIncrement();
}
}
如上,原子类通过 getAndIncrement()
方法来充当 i++,而原子类的 get()
方法就是用来获取当前的值。使用原子类与使用 synchronized 锁在一定程度上性能是比较好的。
什么是 CAS ?
CAS(Compare and swap),也就是比较并交换,他是一种无锁的(非阻塞)原子性操作机制,用于实现并发环境下的线程安全。
CAS 操作数
CAS 的伪代码描述:
java
CAS(V, A, B) {
if (V == A) {
V = B
return true
} else {
return false
}
}
CAS 有 3 个操作数:
- V:内存中的当前值(待修改的目标变量)。
- A:旧的预期值(线程认为变量当前应该是什么)。
- B:新值(线程希望将变量更新为什么)。
当且仅当内存值 V 等于旧的预期值 A 相同时,将内存值 V 修改为 B,否则无论什么值都会进行自旋(不断的重试)。
源码解读
我们通过 JDK 的 AtomicInteger
类来进行研究。
先来体验一下,通过 AtomicInteger
的 compareAndSet
方法,先初始化值为 1,通过 CAS 来将值改为 2025,会先判断现在内存的值是不是等于旧的预期值,其会返回一个 Boolean 值。
java
public class CasDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(1);
System.out.println(atomicInteger.compareAndSet(1, 2025) + "\t" + atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(1, 2025) + "\t" + atomicInteger.get());
}
}
// 执行结果
true 2025
false 2025
硬件级保证
CAS 是 JDK 提供的非阻塞原子性操作,其是通过硬件来保证比较并更新的原子性。
CAS 是一条 CPU 的原子指令(cmpxchg 指令),这样不会造成数据不一致的问题,Unsafe 提供的 CAS 方法的底层就是 CPU 指令 cmpxchg。
执行 cmpxchg 指令的时候,会判断当前系统是否为多核系统,如果是则会给总线枷锁,只有一个线程会对总线加锁成功,加锁成功会执行 CAS 操作。
compareAndSet 源代码
AtomicInteger
的 compareAndSet
实际上就是使用了 unsafe
类。
在进一步看到 unsafe.class 中的方法:
java
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
这三个方法都是类似的,主要还是这四个参数:
- var1:表示要操作的对象
- var2:表示要操作的对象中属性的地址偏移量
- var4:表示需要修改数据的期望值
- var5/var6:表示需要修改成为的新值
Unsafe 类
Unsafe
类是 Java 中一个特殊的工具类(位于 sun.misc 包下),它提供了一系列直接操作内存、执行底层硬件原子操作的方法。这些方法绕过了 Java 语言的安全检查机制(如数组越界、对象构造限制等),因此被称为"不安全"(Unsafe)。它是 Java 并发编程和高性能库的基石,但普通开发者通常不直接使用它,而是通过更上层的 API(如 AtomicInteger、AQS)间接调用其功能。
unsafe
是 CAS 的核心类,Java 是无法直接访问底层系统,需要通过本地方法(native)方法来访问。其内部方法可以像 C 的指针一样直接操作内存,Java 中的 CAS 操作的执行也都依赖于 unsafe 类。
变量
valueOffset
,表示变量值在内存中的偏移地址,unsafe 是根据内存偏移地址来进行获取数据。变量
value
使用 volatile 修饰,确保了变量在多线程之间的内存可见性。
如看以下的 AtomicInteger.getAndIncrement()方法。
实际上最终调用的是 unsafe.getAndAddInt() 方法,其中的 var1 表示对象,var2 是对象在内存地址的偏移量,var4 是要增加的值,var5 是当前共享内存的最新值。这里会有 do-while,首先会获取当前地址中的内存值,调用 CAS 操作,如果 CAS 操作返回 false,就会再次进入循环,直到 CAS 操作成功。
底层汇编源码
在 JDK 的 unsafe.cpp 中,可以以下的代码,核心就是通过执行Atomic的cmpxchg函数进行原子比较交换。
java
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
// We are about to write to this entry so check to see if we need to copy it.
// 执行了一个写屏障操作(write barrier),用于保证在修改对象之前进行必要的处理。
// JNIHandles::resolve(obj)将obj从JNI句柄解析为Java对象,并使用oopDesc::bs()执行写屏障操作。
oop p = oopDesc::bs()->write_barrier(JNIHandles::resolve(obj));
// 计算变量value在内存中的地址,根据valueOffset来计算value的地址
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);// 计算出要进行原子比较和交换操作的内存地址
// 调用Atomic的cmpxchg函数来进行比较交换,其中x是比交换的值,e是比较的值
// CAS 成功,返回期望值e,则会返回true
// CAS 失败,返回的是内存的value值,与e不同,会返回false
return (jint)(Atomic::cmpxchg(x, addr, e)) == e; // 核心代码 执行原子的比较和交换操作
UNSAFE_END
在 cmpxchg 底层逻辑是会根据不同的操作系统去重载不同的 cmpxchg 函数,这里最主要的是需要直到 CAS 机制是通过硬件实现的,在硬件层面上提升效率。最底层通过硬件来保证原子性和可见性,实现就是在硬件平台的汇编指令。
原子引用类型
JDK 提供了许多的 Atomic 类型,包括 AtomicInteger、AtomicLong、AtomicReference 等 API,当需要其他的原子类型呢,这时候 AtomicReference 就发挥了作用,看以下例子:
java
public class AtomicReferenceDemo {
public static void main(String[] args) {
AtomicReference<Car> carAtomicReference = new AtomicReference<>();
Car bez = new Car("奔驰", "GLC", 40);
Car audi = new Car("奥迪", "A4L", 30);
Car mi = new Car("小米", "SU7", 30);
carAtomicReference.set(bez);
System.out.println(carAtomicReference.compareAndSet(bez, audi) + "\t" + carAtomicReference.get());
System.out.println(carAtomicReference.compareAndSet(bez, mi) + "\t" + carAtomicReference.get());
}
}
@Data
@AllArgsConstructor
class Car {
String brand;
String model;
int price;
}
// 输出
true Car(brand=奥迪, model=A4L, price=30)
false Car(brand=奥迪, model=A4L, price=30)
创建了三个对象,先给原子类设置 bez
对象,调用两次 CAS 得到结果,其 CAS 效果也是明显的。
CAS 与自旋锁
自旋锁(Spin Lock)是一种基于忙等待的锁机制。线程在获取锁失败时,循环尝试(自旋)而非阻塞,直到锁被释放。CAS 是自旋锁的基础,通过 CPU 指令来保证原子性,自旋是尝试获取锁的线程不会立即阻塞,是不断地循环来尝试获取锁,如果发现锁被占用的时候,会不断循环判断锁状态,直到获取锁。其好处是减少了线程上下文切换的消耗,坏处是循环会消耗 CPU。
如下通过原子类来实现自旋锁的案例:
java
public class SpinLockDemo {
AtomicReference<Thread> atomicReference = new AtomicReference<>();
public void lock() {
// 获取当前线程
Thread currentThread = Thread.currentThread();
while (!atomicReference.compareAndSet(null, currentThread)) {
// System.out.println(currentThread.getName() + " 自旋中...");
}
System.out.println(currentThread.getName() + " 获取锁");
}
public void unLock() {
// 获取当前线程
Thread currentThread = Thread.currentThread();
atomicReference.compareAndSet(currentThread, null);
System.out.println(currentThread.getName() + " 释放锁");
}
public static void main(String[] args) {
SpinLockDemo spinLockDemo = new SpinLockDemo();
new Thread(() -> {
spinLockDemo.lock();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
spinLockDemo.unLock();
}, "T1").start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new Thread(() -> {
spinLockDemo.lock();
spinLockDemo.unLock();
}, "T2").start();
}
}
// 执行结果
T1 获取锁
// .... 过了5秒
T1 释放锁
T2 获取锁
T2 释放锁
如以上案例,我们通过 CAS 机制来实现一个简单的自旋锁,在上锁的时候,我们第一次通过 CAS 判断一开始内存是否为空,也就是没有设置线程,如果 CAS 判断为 true,也就是没有线程获取这把锁,那就把AtomicReference 设置当前线程,如果 CAS 为 flase,取反之后就是 true,也就会不断地自旋,等待解锁将其设置为 null。因此执行后,在 T1 获取锁之后,T2 进来会不断循环,等 T1 执行 unlock 的时候去将AtomicReference 设置为 null。
CAS 优缺点
首先是 CAS 的优点,CAS 的高性能,能够避免线程阻塞,适合低竞争场景。其次是无锁设计,能够减少死锁风险,代码更简洁。
其次是 CAS 的缺点:
- ① 自旋开销大:从 getAndAddInt() 方法中可以看出,他是通过 do-while 形式,一旦失败,会一直尝试,如果一直不成功,可能会对 CPU 带来很大的开销。
- ② ABA问题:若变量值从A→B→A,CAS可能误判未被修改。解决方案:使用版本号标记(如AtomicStampedReference)
ABA 问题
ABA问题 是CAS(Compare and Swap)操作中的一个经典并发问题。它发生在以下场景中:
- 线程1读取内存值为 A,准备将其修改为 C。
- 在线程1执行 CAS 之前,线程2 将值从 A 改为 B,接着又改回 A。
- 线程1执行 CAS 时,发现内存值仍然是 A,于是操作成功。
- 虽然最终值仍是A,但中间经历了A→B→A的变化,可能导致逻辑错误。
AtomicStampedReference
我们先通过单线程的角度来学习这个AtomicStampedReference 如何使用版本号来实现解决 ABA。
java
public class ABADemo {
public static void main(String[] args) {
Car car = new Car("benz", "GLC", 40);
// 采用版本号,一开始默认为1
AtomicStampedReference<Car> stampedReference = new AtomicStampedReference<>(car, 1);
System.out.println("开始值" + "\t" + stampedReference.getReference() + "\t" + stampedReference.getStamp());
Car audi = new Car("奥迪", "A4L", 30);
boolean judgment;
judgment = stampedReference.compareAndSet(car, audi, stampedReference.getStamp(), stampedReference.getStamp() + 1);
System.out.println(judgment + "\t" + stampedReference.getReference() + "\t" + stampedReference.getStamp());
judgment = stampedReference.compareAndSet(audi, car, stampedReference.getStamp(), stampedReference.getStamp() + 1);
System.out.println(judgment + "\t" + stampedReference.getReference() + "\t" + stampedReference.getStamp());
}
}
// 结果
开始值 Car(brand=benz, model=GLC, price=40) 1
true Car(brand=奥迪, model=A4L, price=30) 2
true Car(brand=benz, model=GLC, price=40) 3
每次进行 CAS 的时候我们需要将版本号+1,这样当我们改动过数据在改回来的时候版本号会一直累加。
接下来继续学习在多线程中的场景:
java
public class ABADemo2 {
static AtomicInteger atomicInteger = new AtomicInteger(1);
public static void main(String[] args) {
new Thread(() -> {
// 在线程A中,我们会将其设置2
atomicInteger.compareAndSet(1, 2);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// ABA 问题
atomicInteger.compareAndSet(2, 1);
}, "A").start();
new Thread(() -> {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// B线程获取并修改
System.out.println("CAS状态:" + atomicInteger.compareAndSet(1, 3) + ",B执行的结果值 " + atomicInteger.get());
}, "B").start();
}
}
// 结果
CAS状态:true,B执行的结果值 3
在多线程中,A 线程首先将 1 通过 CAS 改成了 2,再通过 CAS 改回来 1,等线程 B 执行的时候,进行 CAS 时,会发现内存中还是 1,就会以为没有改动过,结果就会修改为 3。这个情况就是发生了 ABA 问题。再来看以下例子:
java
public class ABADemo3 {
static AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(1, 1);
public static void main(String[] args) {
new Thread(() -> {
// 获取版本号
int stamp = stampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + " 首次版本号:" + stamp);
// 确保B线程拿到的是一样的
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
stampedReference.compareAndSet(1, 2, stampedReference.getStamp(), stampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + " 第1次修改后版本号:" + stampedReference.getStamp());
// 第二次修改回去
stampedReference.compareAndSet(2, 1, stampedReference.getStamp(), stampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + " 第2次修改后版本号:" + stampedReference.getStamp());
}, "A").start();
new Thread(() -> {
// 获取版本号
int stamp = stampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + " 首次版本号:" + stamp);
// 等A线程执行完毕
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 这里用stamp是个缓存值,主要是为了模拟获取期望版本号的时候是与A的首次一致
boolean b = stampedReference.compareAndSet(1, 3, stamp, stamp + 1);
System.out.println(Thread.currentThread().getName() + " 第1次修改是否成功:" + b + ",当前版本号:" + stampedReference.getStamp());
}, "B").start();
}
}
// 执行结果
A 首次版本号:1
B 首次版本号:1
A 第1次修改后版本号:2
A 第2次修改后版本号:3
B 第1次修改是否成功:false,当前版本号:3
首先我们模拟了两个线程在执行的时候,首先 A 与 B 线程都是获得的版本号:1,A 线程进行了 ABA 问题复现,线程 B 在最后执行 CAS 的时候就能够看到是解决了 ABA 问题,案例中 B 线程使用缓存值是为了模拟获取期望版本号的时候是一开始的首次版本号。
总结
本文详细介绍了 CAS(Compare-and-Swap,比较并交换)机制及其在 Java 并发编程中的应用。CAS 是一种无锁的同步机制,通过硬件级别的原子操作,实现了高效的线程安全操作。它在 Java 的 java.util.concurrent.atomic
包中得到了广泛应用,如 AtomicInteger 、AtomicLong 等原子类,这些类通过 CAS 提供了高性能的线程安全操作。 CAS 在自旋锁中的应用。自旋锁是一种基于忙等待的锁机制,通过不断尝试获取锁来避免线程阻塞。结合 CAS,自旋锁能够有效减少线程上下文切换的开销,但也需要谨慎使用,以避免过度占用 CPU 资源。
总体而言,CAS 是一种高效、简洁的并发控制机制,适用于低竞争场景。开发者在使用时需要权衡其优缺点,并结合实际需求选择合适的并发策略。掌握 CAS 的原理和应用,能够帮助开发者更好地优化并发程序的性能和可靠性。
转发请携带作者信息 @怒放吧德德 @一个有梦有戏的人
持续创作很不容易,作者将以尽可能的详细把所学知识分享各位开发者,一起进步一起学习。转载请携带链接,转载到微信公众号请勿选择原创,谢谢!
👍创作不易,如有错误请指正,感谢观看!记得点赞哦!👍
谢谢支持!