Compare And Swap(CAS)总结

本文详细介绍了CAS(Compare-And-Swap)的基本概念、原理,展示了JDK中Unsafe类的使用以及自旋锁的实现。讨论了CAS的优缺点,包括长时间自旋带来的开销和ABA问题,并给出了在解决ABA问题上的改进方法。最后总结了CAS的适用场景和关键知识点。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录​​​​​​​

一、什么是CAS

二、CAS的原理分析

三、JDK中对CAS的支持 — Unsafe类

Unsafe底层汇编源码分析

四、自旋锁

五、CAS的缺点

六、CAS的使用场景


一、什么是CAS

CAS的全称是Compare-And-Swap,它是一条CPU的原子指令,不会有线程安全问题,CAS是实现并发算法时常用到的一种技术。

CAS是解决多线程并行情况下使用锁造成性能损耗的一种机制,CAS操作包含三个操作数——内存位置(V)、预期值(A)和新值(B)如果内存位置的值与预期值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作,多个线程同时执行CAS操作只有一个会成功

二、CAS的原理分析

CAS操作的流程图如下:

CAS是JDK提供的非阻塞原子性操作,它通过硬件保证了比较-更新的原子性。

CAS是一条CPU的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,Unsafe提供的CAS方法,底层实现即为CPU指令cmpxchg。

执行cmpxchg指令的时候,会判断当前系统是否为多核系统,如果是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功之后会执行cas操作,也就是说CAS的原子性实际上是CPU实现独占的,比起使用synchronized重量级锁,cas的排它时间要短很多,所有在多线程情况下性能会比较好。

三、JDK中对CAS的支持 — Unsafe类

Unsafe是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(Native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定的内存数据,Unsafe类存在sun.misc包中,其内部方法操作可以像C指针一样直接操作内存,因为Java中的CAS操作的执行依赖于Unsafe类的方法。

注意Unsafe类的所有方法都是native修饰的,也就是说unsafe类中的方法都直接调用操作系统底层资源执行相应的任务。

  • 1)、变量valueOffset,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。
  • 2)、变量value使用volatile修饰,保证了多线程之间的内存可见性。

Unsafe类包含三个重要方法:

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类使Java拥有了像C语言的指针一样操作内存空间的能力,同时也带来了指针的问题。过度的使用Unsafe类会使得出错的几率变大,因此Java官方并不建议使用的,官方文档也几乎没有。Unsafe对象不能直接调用,只能通过反射获得。

Unsafe底层汇编源码分析

Unsafe类中的compareAndSwapInt,是一个native本地方法,该方法的实现位于unsafe.cpp中:

// unsafe.cpp
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);
  // 根据内存偏移地址offset,计算变量value在内存中的地址
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  // 调用Atomic的cmpxchg函数执行比较并交换
  // x: 即将更新的值
  // addr: 内存地址
  // e: 原内存的值
  // cas成功,返回true; cas失败,返回false
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;

// atomic.cpp
// 根据操作系统类型调用不同平台下的重载函数
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);
}

四、自旋锁

CAS是实现自旋锁的基础,CAS利用CPU指令保证了操作的原子性,以达到锁的效果。自旋,是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,当线程发现锁被占用时,会不断循环判断锁的状态,直到获取。这样的好处是减少线程上下文切换的消耗,缺点是循环重试将会消耗CPU。

模拟实现一个简单的自旋锁:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

/**
 * 模拟实现自旋锁
 */
public class SpinLockDemo {

    /**
     * 原子引用类型: 保存当前获取锁的线程
     */
    AtomicReference<Thread> atomicReference = new AtomicReference<>();

    public void lock() {
        Thread currentThread = Thread.currentThread();
        System.out.println(currentThread.getName() + "尝试获取锁...");

        // 预期值: null(没人加锁)   新值: 当前线程
        while (!atomicReference.compareAndSet(null, currentThread)) {
            System.out.println(currentThread.getName() + "自旋等待获取锁");
        }

        System.out.println(currentThread.getName() + "成功获取锁...");
    }

    public void unlock() {
        Thread currentThread = Thread.currentThread();
        // 预期值: 当前线程   新值: null(没人加锁)
        atomicReference.compareAndSet(currentThread, null);
        System.out.println(currentThread.getName() + "释放锁...");
    }

    public static void main(String[] args) {
        SpinLockDemo spinLockDemo = new SpinLockDemo();
        new Thread(() -> {
            // t1线程先加锁
            spinLockDemo.lock();
            try {
                TimeUnit.MILLISECONDS.sleep(1500);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            // 1.5秒后,t1线程释放锁
            spinLockDemo.unlock();
        }, "t1").start();

        // 休眠1秒,确保t1线程先获取到锁资源
        try {
            TimeUnit.MILLISECONDS.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        new Thread(() -> {
            spinLockDemo.lock();
            spinLockDemo.unlock();
        }, "===t2").start();
    }
}

运行结果:

t1尝试获取锁...
t1成功获取锁...
===t2尝试获取锁...
===t2自旋等待获取锁
===t2自旋等待获取锁
//......
===t2自旋等待获取锁
t1释放锁...
===t2成功获取锁...
===t2释放锁...

可以看到,首先t1线程获取锁资源,这时候t2也去尝试获取锁,因为锁已经被t1获取了,所以获取失败,此时t2线程就一直在那里自旋,简单理解,就是在那里循环等待获取锁资源,一旦t1释放锁,t2也就结束自旋了,成功获取到锁,这就是简单的自旋锁。

五、CAS的缺点

  • 1)、长时间自旋,开销大

我们看sun.misc.Unsafe#getAndAddInt的源码:

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

我们看到,getAndAddInt()方法内部有个do while循环,如果CAS失败,会一直进行尝试。如果长时间一直不成功,相当于CPU空转,可能会给CPU带来很大的开销。

  • 2)、只能保证一个共享变量的原子操作

CAS只能保证对一个共享变量执行操作的原子性,当对多个共享变量操作时,循环CAS就无法保证操作的原子性。此时我们可以选择使用加锁的方式,或者将多个共享变量合并成一个变量,然后再进行CAS操作这个合并后的变量。从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。

  • 3)、ABA问题

CAS算法实现一个重要前提是需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差内可能会导致数据的变化。

比如说一个线程1从内存位置V中取出A,这时候另外一个线程2也从内存中读取出A,并且线程2进行了一些操作将值变成了B,然后线程2又将数据变成A,这时候线程1进行CAS操作发现内存中的值还是A,预期成功,然后线程1操作成功。

尽管线程1的CAS操作成功, 但是不代表这个过程中,它的值没有发生变化。

演示一下ABA问题:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class ABADemo {
    static AtomicInteger atomicInteger = new AtomicInteger(100);

    public static void main(String[] args) {
        // 在t2线程执行期间, t1线程完成了一次A->B->A的操作.
        new Thread(() -> {
            // A -> B
            atomicInteger.compareAndSet(100, 101);

            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            // B -> A
            atomicInteger.compareAndSet(101, 100);
        }, "t1").start();


        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(2);
                boolean result = atomicInteger.compareAndSet(100, 102);
                System.out.println("result = " + result);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }, "t2").start();
    }
}

运行结果:

result = true

可以看到, 在t2线程执行期间, t1线程完成了一次A->B->A的操作,此时t2执行CAS操作,比对内存中的值,跟当初获取到的值一样,还是100,以为内存中的值没人修改过,所以t2的CAS操作执行成功,但是实际上,这期间,内存中的值还是有可能被其它线程修改过的,这就是ABA问题。

要解决ABA问题也很简单,增加一个版本号即可。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A了。在JDK中,提供了AtomicStampedReference类用来解决ABA问题,下面我们看如何使用AtomicStampedReference解决这个问题的。

public class ABADemo2 {
    /**
     * 基于时间戳的原子引用,解决ABA问题
     */
    static AtomicStampedReference atomicStampedReference = new AtomicStampedReference(100, 1);

    public static void main(String[] args) {
        // 在t2线程执行期间, t1线程完成了一次A->B->A的操作.
        new Thread(() -> {
            int firstStamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "第一次版本号 = " + firstStamp);

            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            // 1A -> 2B
            atomicStampedReference.compareAndSet(100, 101, firstStamp, firstStamp + 1);

            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            int secondStamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "第二次版本号 = " + secondStamp);

            // 2B -> 3A
            atomicStampedReference.compareAndSet(101, 100, secondStamp, secondStamp + 1);

            System.out.println(Thread.currentThread().getName() + "第三次版本号 = " + atomicStampedReference.getStamp());
        }, "t1线程").start();


        new Thread(() -> {
            try {
                int firstStamp = atomicStampedReference.getStamp();
                System.out.println(Thread.currentThread().getName() + "第一次版本号 = " + firstStamp);

                TimeUnit.SECONDS.sleep(3);
                boolean result = atomicStampedReference.compareAndSet(100, 102, firstStamp, firstStamp + 1);
                System.out.println(Thread.currentThread().getName() + "是否修改成功: " + result + ", 当前版本号 = " + atomicStampedReference.getStamp() + ", 当前值 = " + atomicStampedReference.getReference());
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }, "t2线程").start();
    }
}

执行结果:

t1线程第一次版本号 = 1
t2线程第一次版本号 = 1
t1线程第二次版本号 = 2
t1线程第三次版本号 = 3
t2线程是否修改成功: false, 当前版本号 = 3, 当前值 = 100

从运行结果,我们可以看到,一开始,两个线程t1、t2拿到的初始版本号都是1,但是t2线程修改期间,t1线程已经完成了一次A -> B -> A的修改过程,由于使用了AtomicStampedReference带有时间戳的原子引用,实际上t1线程修改的时候,已经变成了1A -> 2B -> 3A了,所以当t2线程执行cas时,虽然期望值还是100,但是由于版本号对不上了,所以t2线程cas执行失败。以上就是使用AtomicStampedReference解决了ABA问题。

六、CAS的使用场景

CAS适用在轻度竞争(线程较少并且处理时间较短)的情况下,减少了线程间的状态切换(线程状态切换是需要CPU调度的),但是CAS是一个占用CPU资源的操作,如果操作处理时间长或者大量线程竞争,则CAS的成功率太低就会导致CPU资源的浪费。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值