CAS(比较并交换)
一、CAS是什么?
1、简介
在Java中,锁在并发处理中占据了一席之地,但是使用锁有一个不好的地方,就是当一个线程没有获取到锁时会被阻塞挂起,这会导致线程上下文的切换和重新调度开销。Java提供了非阻塞的volatile关键字来解决共享变量的可见性问题,这在一定程度上弥补了锁带来的开销问题,但是volatile只能保证共享变量的可见性,不能解决读一改一写等的原子性问题。CAS即CompareandSwap,其是JDK提供的非阻塞原子性操作,它通过硬件保证了比较一更新操作的原子性。(乐观锁实现方式)
2、核心定义
CAS(Compare-And-Swap),中文译为比较并交换,是一种无锁并发的原子操作算法。它包含三个核心操作数:
- 内存位置(V) - 要操作的内存地址
- 预期原值(A) - 读取到的旧值
- 新值(B) - 想要更新的值
它包含3 个参数 CAS(V,E,N),V表示要更新变量的值,E表示预期值,N表示新值。仅当 V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程则什么都不做。最后,CAS 返回当前V的真实值。
CAS的伪代码逻辑非常简单:
public boolean compareAndSwap(V, A, B) {
if (V == A) { // 如果内存值等于预期值
V = B; // 用新值替换旧值
return true; // 操作成功
}
return false; // 操作失败
}
但关键在于:这个“比较-更新”过程必须是原子的——在它执行期间,其他线程无法修改V的值。
二、CAS底层原理
1、CAS执行步骤
第一步:
private volatile int value; // 1. volatile保证可见性
让value值可见,让它可以参与多线程任务。
第二步:
private static final long valueOffset; // 2. 内存偏移量
private static final Unsafe unsafe = Unsafe.getUnsafe();// 3. 获取Unsafe单例
定义final形式的内存偏移量,不可以修改。
第三步:
static {
try {
// 4. 计算value字段在对象内存中的偏移位置
valueOffset = unsafe.objectFieldOffset(
AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
这里的关键是内存偏移量。每个Java对象字段在内存中都有固定位置,valueOffset就是value字段的“内存地址坐标”。
第四步:
// 4. 核心的incrementAndGet方法
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
第五步:
// Unsafe.java 关键方法
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
// 1. 获取当前最新值(volatile读)
v = getIntVolatile(o, offset);
// 2. 尝试CAS:如果当前值仍为v,则更新为v+delta
} while (!compareAndSwapInt(o, offset, v, v + delta));
// 3. 如果失败(其他线程修改了值),则循环重试
return v; // 返回旧值
}
这就是CAS的乐观锁思想:假设冲突很少发生,失败了就重试,直到成功为止。
2、CAS底层逻辑
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
// 步骤1:获取当前最新值
var5 = this.getIntVolatile(var1, var2);
// 步骤2:尝试CAS更新(失败则循环重试)
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5; // 返回更新前的值
}
- var1:AtomicInteger这个对象a;
- var2:偏移量(有效地址);
- var5:AtomicInteger这个对象a在地址var2上的期待值;
- var4: 要增加的值(delta)
- var5+var4:是值+1操作
到了这一步就稍微有点眉目了,原来底层调用的是compareAndSwapInt方法,这compareAndSwapInt方法其实就是CAS机制。因此如果我们想搞清楚AtomicInteger的原子操作是如何实现的,我们就必须要把CAS机制搞清楚,这也是为什么我们需要掌握CAS机制的原因。
想要弄清楚其底层原理,深入到源码是最好的方式,在上面我们已经通过源码看到了其实就是Usafe的方法来完成的,在这个方法中使用了compareAndSwapInt这个CAS机制。因此,现在我们有必要进一步深入进去看看:
public final class Unsafe {
// compareAndSwapInt 是 native 类型的方法
public final native boolean compareAndSwapInt(
Object o,
long offset,
int expected,
int x
);
//剩余还有很多方法
}
我们可以看到这里面主要有四个参数
- 第一个参数就是我们操作的对象a,
- 第二个参数是对象a的地址偏移量,有效地址
- 第三个参数表示我们期待这个a是什么值
- 第四个参数表示的是a的实际值。
这里我们会发现这个compareAndSwapInt是一个native方法(是Java中调用非Java代码(通常是C/C++)的方法。它是Java与底层操作系统、硬件或其他语言库之间的桥梁。),也就是说再往下走就是C语言代码,如果我们保持好奇心,可以继续深入进去看看。
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe,
jobject obj, jlong offset, jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj);
// 根据偏移量valueOffset,计算 value 的地址
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
// 调用 Atomic 中的函数 cmpxchg来进行比较交换
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
上面的代码我们解读一下:首先使用jint计算了value的地址,然后根据这个地址,使用了Atomic的cmpxchg方法进行比较交换。现在问题又抛给了这个cmpxchg,真实实现的是这个函数。我们再进一步深入看看,真相已经离我们不远了。
unsigned Atomic::cmpxchg(unsigned int exchange_value,
volatile unsigned int* dest,
unsigned int compare_value) {
assert(sizeof(unsigned int) == sizeof(jint), "more work to do");
/*
* 根据操作系统类型调用不同平台下的重载函数,
这个在预编译期间编译器会决定调用哪个平台下的重载函数
*/
return (unsigned int)Atomic::cmpxchg((jint)exchange_value,
(volatile jint*)dest, (jint)compare_value);
}
在在不同的操作系统下会调用不同的cmpxchg重载函数
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest,
jint compare_value) {
int mp = os::is_MP();
__asm {
mov edx, dest
mov ecx, exchange_value
mov eax, compare_value
LOCK_IF_MP(mp)
cmpxchg dword ptr [edx], ecx
}
}
这块的代码就有点涉及到汇编指令相关的代码了,到这一步就彻底接近真相了,首先三个move指令表示的是将后面的值移动到前面的寄存器上。然后调用了LOCK_IF_MP和下面cmpxchg汇编指令进行了比较交换。现在我们不知道这个LOCK_IF_MP和cmpxchg是如何交换的,没关系我们最后再深入一下。
inline jint Atomic::cmpxchg (jint exchange_value,
volatile jint* dest, jint compare_value) {
//1、 判断是否是多核 CPU
int mp = os::is_MP();
__asm {
//2、 将参数值放入寄存器中
mov edx, dest
mov ecx, exchange_value
mov eax, compare_value
//3、LOCK_IF_MP指令
cmp mp, 0
//4、 如果 mp = 0,表明线程运行在单核CPU环境下。此时 je 会跳转到 L0 标记处,直接执行 cmpxchg 指令
je L0
_emit 0xF0
//5、这里真正实现了比较交换
L0:
/*
* 比较并交换。简单解释一下下面这条指令,熟悉汇编的朋友可以略过下面的解释:
* cmpxchg: 即“比较并交换”指令
* dword: 全称是 double word 表示两个字,一共四个字节
* ptr: 全称是 pointer,与前面的 dword 连起来使用,表明访问的内存单元是一个双字单元
* 这一条指令的意思就是:
将 eax 寄存器中的值(compare_value)与 [edx] 双字内存单元中的值进行对比,
如果相同,则将 ecx 寄存器中的值(exchange_value)存入 [edx] 内存单元中。
*/
cmpxchg dword ptr [edx], ecx
}
}
到这一步了,相信你应该理解了这个CAS真正实现的机制了吧,最终是由操作系统的汇编指令完成的。
3、Unsafe类讲解
Unsafe类是Java中的一个底层类,提供了硬件级别的原子操作,这个类通常不直接暴露给普通Java应用开发者使用,而是被内部类如AtomicIntege所使用,Unsafe类提供了如compareAndSwapInt等方法,这些方法可以原子地更新内存中的值。
1、核心特点
- 直接内存操作:像C语言一样操作内存
- 原子操作:提供硬件级别的CAS操作
- 绕过安全机制:可以修改final字段、创建未初始化对象
- 线程控制:提供底层的线程挂起和恢复
你希望将Unsafe类中这几个核心的native方法整理成一个清晰的表格,方便查阅和记忆。我已经按照方法名、方法作用、核心参数/特点三个维度为你整理好了这份表格,内容如下:
| 方法签名 | 方法作用 | 核心参数/特点 |
|---|---|---|
long objectFieldOffset(Field field) |
返回指定字段在所属类中的内存偏移地址,仅在Unsafe访问该字段时使用 | 参数:目标字段Field对象 返回值:内存偏移地址(long型) |
int arrayBaseOffset(Class arrayClass) |
获取指定数组类中第一个元素的内存地址(数组基地址) | 参数:数组的Class对象(如int[].class) 返回值:第一个元素的地址偏移(int型) |
int arrayIndexScale(Class arrayClass) |
获取指定数组类中单个元素占用的字节数 | 参数:数组的Class对象 返回值:单个元素字节数(int型),如int数组返回4、long数组返回8 |
boolean compareAndSwapLong(Object obj, long offset, long expect, long update) |
CAS核心方法:比较obj中offset偏移处的long值是否等于expect,相等则更新为update,返回true;否则返回false | 核心:原子性的比较并替换,无锁操作 参数:目标对象、字段偏移量、期望原值、更新值 |
long getLongVolatile(Object obj, long offset) |
获取obj中offset偏移处的long值,遵循volatile语义(保证可见性、禁止指令重排) | 特点:强制从主内存读取,而非线程本地缓存 |
void putLongVolatile(Object obj, long offset, long value) |
设置obj中offset偏移处的long值为value,遵循volatile语义 | 特点:强制写入主内存,立即可见给其他线程 |
void putOrderedLong(Object obj, long offset, long value) |
延迟版的putLongVolatile:设置obj中offset偏移处的long值,但不保证修改对其他线程立刻可见 | 适用场景:变量用volatile修饰,且无需立即同步,性能优于putLongVolatile |
long getAndSetLong(Object obj, long offset, long update) |
原子性获取obj中offset偏移处long值的当前值(volatile语义),并将其更新为update | 核心:原子的“获取-设置”操作,返回更新前的原值 |
2、内存操作功能
直接内存读写
// 1. 对象字段读写
public native int getInt(Object obj, long offset);
public native void putInt(Object obj, long offset, int value);
// 2. 绝对地址读写(危险!)
public native int getInt(long address);
public native void putInt(long address, int value);
// 3. 数组操作支持
public native int arrayBaseOffset(Class<?> arrayClass);
public native int arrayIndexScale(Class<?> arrayClass);
// 示例:直接操作数组
int[] array = new int[10];
Unsafe unsafe = getUnsafeInstance();
long baseOffset = unsafe.arrayBaseOffset(int[].class);
int scale = unsafe.arrayIndexScale(int[].class);
// 直接修改数组元素(绕过边界检查)
for (int i = 0; i < array.length; i++) {
long offset = baseOffset + (i * scale);
unsafe.putInt(array, offset, i * 100); // 直接写入内存
}
内存分配与释放
// 分配堆外内存(DirectByteBuffer底层使用)
public native long allocateMemory(long bytes);
// 重新分配内存
public native long reallocateMemory(long address, long bytes);
// 释放内存
public native void freeMemory(long address);
// 内存初始化
public native void setMemory(Object obj, long offset, long bytes, byte value);
// 内存复制
public native void copyMemory(Object src, long srcOffset,
Object dest, long destOffset, long bytes);
// 示例:创建堆外内存缓冲区
public class DirectBuffer {
private long address;
private long size;
public DirectBuffer(int capacity) {
this.size = capacity;
this.address = unsafe.allocateMemory(capacity);
unsafe.setMemory(address, capacity, (byte) 0); // 初始化为0
}
public void put(int index, byte value) {
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException();
}
unsafe.putByte(address + index, value);
}
public byte get(int index) {
return unsafe.getByte(address + index);
}
@Override
protected void finalize() throws Throwable {
unsafe.freeMemory(address); // 手动释放内存
super.finalize();
}
}
3、getAndAddInt 方法详解
// AtomicInteger.incrementAndGet()底层调用
public final int getAndAddInt(Object obj, long offset, int delta) {
int v;
do {
// 1. volatile读,保证可见性
v = this.getIntVolatile(obj, offset);
// 2. 尝试CAS更新,失败则循环重试
} while (!this.compareAndSwapInt(obj, offset, v, v + delta));
return v; // 返回旧值
}
// 示例:实现线程安全的计数器
public class ThreadSafeCounter {
private volatile int count;
private static final Unsafe unsafe;
private static final long offset;
static {
try {
unsafe = getUnsafeInstance();
offset = unsafe.objectFieldOffset(
ThreadSafeCounter.class.getDeclaredField("count"));
} catch (Exception e) { throw new Error(e); }
}
public int increment() {
int current;
do {
current = unsafe.getIntVolatile(this, offset);
} while (!unsafe.compareAndSwapInt(this, offset, current, current + 1));
return current + 1;
}
}
4、测试用例
public class TestUnSafe {
//获取Unsafe的实例(2.2.1)
static final Unsafe unsafe = Unsafe.getUnsafe();
//记录变量state在类TestUnSafe中的偏移值(2.2.2)
static final long stateOffset;
//变量(2.2.3)
private volatile long state = 0;
static {
try {
//获取state变量在类TestUnSafe中的偏移值(2.2.4)
stateOffset = unsafe.objectFieldOffset(TestUnSafe.class.getDeclaredField("state"));
} catch (Exception ex) {
System.out.println(ex.getLocalizedMessage());
throw new Error(ex);
}
}
public static void main(String[] args) {
//创建实例,并且设置state值为1(2.2.5)
TestUnSafe test = new TestUnSafe();
// (2.2.6)
Boolean sucess = unsafe.compareAndSwapInt(test, stateOffset, 0, 1);
System.out.println(sucess);
}
}
在如上代码中,代码(2.2.1) 获取了Unsafe的一个实例,代码(2.2.3) 创建了- - 个变量state并初始化为0。
代码(2.2.4) 使用unsafe.objectFieldOffset获取TestUnSafe类里面的state 变量,在TestUnSafe对象里面的内存偏移量地址并将其保存到stateOffset变量中。
代码(2.2.6)调用创建的unsafe实例的compareAndSwapInt方法,设置test对象的state变量的值。具体意思是,如果test对象中内存偏移量为stateOffset的state变量的值为0,则更新该值为1。
运行上面的代码,我们期望输出true,然而执行后会输出如下结果。


代码(2.2.7) 获取调用getUnsafe这个方法的对象的Class对象,这里是TestUnSafe.class。
代码(2.2.8)判断是不是Bootstrap类加载器加载的localClass,在这里是看是不是Bootstrap 加载器加载了TestUnSafe.class。 很明显由于TestUnSafe.class 是使用AppClassLoader加载的,所以这里直接抛出了异常。
思考一下,这里为何要有这个判断?我们知道Unsafe类是rt.jar包提供的,rt.jar 包里面的类是使用Bootstrap类加载器加载的,而我们的启动main函数所在的类是使用AppClassLoader加载的,所以在main函数里面加载Unsafe类时,根据委托机制,会委托给Bootstrap去加载Unsafe类。
如果没有代码(2.2.8)的限制,那么我们的应用程序就可以随意使用Unsafe做事情了,Unsafe类可以直接操作内存,这是不安全的,所以JDK开发组特意做了这个限制,不让开发人员在正规渠道使用Unsafe类,而是在rt.jar包里面的核心类中使用Unsafe功能。
我们可以通过反射来做
public class TestUnSafe1 {
static final Unsafe unsafe;
static final long stateOffset;
private volatile long state = 0;
static {
try {
//使用反射获取Unsafe的成员变量theUnsafe
Field field = Unsafe.class.getDeclaredField("theUnsafe");
//设置为可存取
field.setAccessible(true);
//获取该变量的值
unsafe = (Unsafe) field.get(null);
//获取state在TestUnSafe中的汇编语言偏移量
stateOffset = unsafe.objectFieldOffset(TestUnSafe.class.getDeclaredField("state"));
} catch (Exception ex) {
System.out.println(ex.getLocalizedMessage());
throw new Error(ex);
}
}
public static void main(String[] args) {
//创建实例,并且设置state值为1(2.2.5)
TestUnSafe1 test = new TestUnSafe1();
// (2.2.6)
Boolean sucess = unsafe.compareAndSwapInt(test, stateOffset, 0, 1);
System.out.println(sucess+"------>"+unsafe.getIntVolatile(test, stateOffset));
Boolean sucess1 = unsafe.compareAndSwapInt(test, stateOffset, 1, 10);
System.out.println(sucess1+"------>"+unsafe.getIntVolatile(test, stateOffset));
Boolean sucess2 = unsafe.compareAndSwapInt(test, stateOffset, 9, 20);
System.out.println(sucess2+"------>"+unsafe.getIntVolatile(test, stateOffset));
}
}
三、CAS的优缺点
1、CAS的优势
- **避免线程阻塞与上下文切换:**cas是一种乐观锁,而且是一种非阻塞的轻量级的乐观锁,什么是非阻塞式的呢?其实就是一个线程想要获得锁,对方会给一个回应表示这个锁能不能获得。在资源竞争不激烈的情况下性能高,相比synchronized重量锁,synchronized会进行比较复杂的加锁,解锁和唤醒操作。
- 缓存友好性
- 并发安全性优势:1、无死锁特性 2、可预测的性能表现
- 系统设计优势: 1、可伸缩性更好 2、支持更细粒度的并发控制
- 硬件层面的优势:1、直接利用CPU原子指令 2、内存模型兼容性好
2、CAS的缺点
- 自旋导致的CPU浪费
- 缓存一致性流量爆炸
- ABA问题
- 只能保证单一变量的原子性
- 需要手动实现重试逻辑
- 需要手动实现重试逻辑
更多推荐




所有评论(0)