在高并发的业务场景下,线程安全可以通过synchronized或Lock来保证同步,从而达到线程安全的目的。但是,synchronized或Lock都是基于互斥锁的思想理念,加锁和释放锁的过程中,都会带来性能损耗问题。
其实,除了synchronized或Lock以外,还可以通过JUC提供的CAS机制,实现一种无锁的非阻塞线程同步方式,保证线程安全。
什么是CAS
CAS(Compare And Swap),是CPU支持的一种对内存中的共享数据进行操作的特殊CPU指令(例如cmpxchg)。
cmpxchg:它用于对内存中的共享数据,进行原子性的读写操作。进行读写操作时,CPU会比较内存中某个值是否和预期值相同,如果相同,则将这个值更新为新值;如果不相同,则不做更新(或者重试)
在实现过程中,并没有使用锁。所以,本质上来讲CAS是一种无锁的线程同步解决方案,可以保证在多线程并发中保证共享资源的原子性操作,相对于synchronized或Lock来说,是一种轻量级的实现方案。
我们在学习JUC并发集合时,可以发现AtomicInteger、ConcurrentHashMap都是基于CAS机制来实现原子性操作
CAS执行流程
- 读取:线程从内存中读取目标变量的当前值(记为
V
)。 - 计算:基于
V
计算期望的新值(记为N
)。 - 比较并交换:通过 CAS 指令判断内存中当前值是否仍为
V
:- 若是,将内存值更新为
N
,操作成功。 - 若否,说明值已被其他线程修改,操作失败,通常重试或放弃。
- 若是,将内存值更新为
可能小编这样总结有点抽象,举个栗子吧🌰🌰🌰
假设你(线程 A)想修改一个共享变量的值,比如 “桌上的苹果数量”,流程是这样的:
-
读取(记为 V)
你先看了一眼桌上的苹果,现在有 3 个(V=3)。 -
计算(得到 N)
你想把苹果数量改成 5 个(比如放进去 2 个),所以期望的新值 N=5。 -
比较并交换
你准备动手前,再确认一次桌上的苹果是不是还是 3 个(和之前读的 V 对比):- 如果还是 3 个(没被别人动过),就把数量改成 5 个,操作成功。
- 如果变成了 4 个(比如另一个人刚放了 1 个),说明被别人改了,你的计划作废,要么重新看当前数量再试一次,要么放弃。
核心逻辑:
CAS 就像 “先确认、再修改”,确保你修改时,变量没有被其他线程动过,从而不用加锁也能保证安全。失败重试的过程,就像你发现苹果数量变了,重新看一眼再操作,直到成功或放弃。
这样就避免了多个人同时抢着改,导致结果混乱的问题(比如两个人同时想把 3 改成 5,不加控制可能最后变成 4,而 CAS 能保证最终是 5)。
上述一系列操作,由CPU指令cmpxchg来保证原子性操作,可以通过Java代码中的while循环再次调用cmpxchg指令进行重试,直到设置成功位置
Unsafe类
Java 代码本身不能直接写 CPU 指令。但可以通过 Unsafe
类的 native 方法,借助 JVM 底层的 C/C++ 代码,间接调用 CPU 的 CAS 指令,从而实现 CAS 机制。
Unsafe实现CAS的工作原理
1. 借助 JNI 调用底层代码
Unsafe
类中的方法大多是本地方法(使用 native
修饰),这些方法没有在 Java 代码中实现具体逻辑,而是通过 JNI 机制调用底层 C/C++ 代码。JNI 是 Java 提供的一种允许 Java 代码和其他语言(如 C、C++)编写的代码进行交互的技术,使得 Java 程序能够访问底层操作系统和硬件的功能。
2. 获取变量的内存偏移量
Unsafe
类提供了 objectFieldOffset
方法,它可以获取指定对象中某个字段相对对象起始地址的偏移量。例如在 AtomicInteger
中,会使用 objectFieldOffset
方法获取 value
字段的偏移量,代码示例如下:
import sun.misc.Unsafe;
public class AtomicInteger {
private static final Unsafe unsafe;
private static final long valueOffset;
private volatile int value;
static {
try {
// 反射获取Unsafe实例
unsafe = Unsafe.getUnsafe();
// 获取value字段的偏移量
valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) {
throw new Error(ex);
}
}
}
这个偏移量用于在后续操作中精准定位到目标变量在内存中的位置,方便直接对内存中的数据进行读写操作。
3. 利用 CPU 指令实现 CAS 操作
以 compareAndSwapInt
方法为例,它是 Unsafe
类中用于实现针对整数类型的 CAS 操作的方法,定义如下:
public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
o
表示要操作的对象;offset
是前面获取到的目标字段的内存偏移量;expected
是预期值,即线程开始操作时读取到的变量值;x
是要更新的新值。
在底层,通过 JNI 调用的 C/C++ 代码会根据不同的 CPU 架构,使用相应的原子指令来实现比较和交换的操作。比如在 x86 架构上,会使用 cmpxchg
指令,该指令会先比较内存中的值和预期值是否相等,如果相等就将内存中的值更新为新值,并设置标志位反映操作是否成功;如果不相等则不进行更新操作。通过这种方式,在硬件层面保证了比较和交换操作的原子性,避免了多线程环境下的竞态条件。
4. 在 Java 中的使用流程
在 Java 代码中,基于 Unsafe
的 CAS 操作通常是在一个循环中进行的。以 AtomicInteger
的自增方法 incrementAndGet
为例,简化后的代码逻辑如下:
public final int incrementAndGet() {
int expect;
do {
// 获取当前值
expect = get();
// 计算新值
} while (!unsafe.compareAndSwapInt(this, valueOffset, expect, expect + 1));
return expect + 1;
}
线程先读取当前值作为预期值,然后计算新值,接着尝试使用 compareAndSwapInt
方法进行 CAS 操作。如果操作失败(说明变量值已被其他线程修改),则重新读取当前值,再次尝试,直到操作成功。
总的来说,Unsafe
类通过 JNI 调用底层代码,利用 CPU 提供的原子指令,实现了高效的 CAS 操作,为 Java 的并发编程提供了重要的基础支持。
CAS缺点
CAS(Compare And Swap,比较并交换)虽然在多线程并发编程中能避免传统锁机制带来的一些性能开销,实现无锁化的同步操作 ,但它也存在以下缺点:
1. ABA 问题
- 问题描述:一个变量的值从 A 变为 B,再变回 A ,在 CAS 检查时,会认为该变量的值没有发生变化,但实际上它已经被修改过了。这可能导致一些隐藏的问题,因为虽然最终值一样,但中间的变化过程可能对业务逻辑有影响。
- 解决方案:可以使用带有版本号的原子引用,如
AtomicStampedReference
,每次变量值发生变化时,版本号也随之递增,在进行 CAS 操作时,不仅要比较值,还要比较版本号,只有值和版本号都符合预期时,才执行更新操作。
2. 循环开销大(自旋时间长)
- 问题描述:当多个线程同时竞争同一个变量进行 CAS 操作时,若 CAS 操作失败,通常会通过自旋(循环重试)的方式再次尝试,直到操作成功。如果并发冲突严重,即很多线程都在同时修改同一个变量,就会导致大量线程长时间自旋,不断占用 CPU 资源,造成 CPU 利用率过高。
- 解决方案:可以设置自旋次数上限,当达到上限后,线程不再自旋,而是采用其他方式(如进入阻塞状态,等待一段时间后再尝试 ,或者使用传统的锁机制)来获取操作权。
3. 只能保证一个共享变量的原子操作
- 问题描述:CAS 操作针对的是单个变量,如果需要对多个共享变量进行原子性的更新操作,CAS 就无法直接满足需求。虽然可以通过将多个变量封装成一个对象,然后对对象的引用进行 CAS 操作,但这种方式不能保证对象内部每个变量的原子性更新。
- 解决方案:可以使用锁机制(如
synchronized
或ReentrantLock
)来保证多个变量操作的原子性,或者使用AtomicReference
将多个变量封装成一个对象,结合自定义的逻辑来实现原子更新。 此外,Java 提供了StampedLock
等工具类,在一定程度上可以处理多个变量的原子操作问题。
4. 代码复杂性增加
- 问题描述:使用 CAS 编写的代码通常需要处理 CAS 操作失败后的重试逻辑、ABA 问题等,相较于简单的加锁操作,代码的逻辑会更加复杂,可读性和可维护性变差。
- 解决方案:在编写代码时,遵循良好的代码注释和设计模式,尽量将复杂的 CAS 操作逻辑封装在独立的方法或类中,提高代码的可读性和可维护性。 同时,开发团队可以进行代码审查,确保代码的正确性和合理性。
总结
CAS(比较并交换)是 CPU 支持的无锁同步机制,通过硬件指令(如 x86 的 cmpxchg)实现原子操作,核心流程为 “读取 - 计算 - 比较并交换”,无需加锁即可保证多线程下共享资源的原子性,是 JUC 中 AtomicInteger、ConcurrentHashMap 等的实现基础。
其通过 Unsafe 类的 native 方法间接调用 CPU 指令:先获取变量内存偏移量,再通过 compareAndSwapXXX 方法原子比较并更新值,失败时通常循环重试。
缺点包括:ABA 问题(可用 AtomicStampedReference 带版本号解决)、高冲突时自旋消耗 CPU、仅支持单个变量原子操作、代码复杂度较高。
相比 synchronized 或 Lock 的互斥锁机制,CAS 是轻量级方案,适用于低冲突场景,能减少锁的性能损耗。
有问题欢迎留言!!!😗
肥嘟嘟左卫门就讲到这里啦,记得一键三连!!!😗