一、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问题
  • 只能保证单一变量的原子性
  • 需要手动实现重试逻辑
  • 需要手动实现重试逻辑
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐