CAS详解-什么是CAS机制?

200 阅读9分钟

什么是CAS?

CAS是Compare-And-Swap(比较并交换)的缩写,是一种轻量级的同步机制,是一个无锁机制的操作,主要用于实现多线程环境下的无锁算法和数据结构,保证了并发安全性。它可以在不使用锁(如synchronized、Lock)的情况下,对共享数据进行线程安全的操作。

CAS操作主要有三个参数:要更新的内存位置、期望的值和新值。CAS操作的执行过程如下:

  • 首先,获取要更新的值的内存地址,记为var。
  • 然后,将期望值expected与var进行比较, 如果两者相等,则将内存位置的值var更新为新值new。
  • 如果两者不相等,则说明有其他线程修改了内存位置的值var,此时CAS操作失败,需要重新尝试。

简单理解就是: CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。

更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B

基本使用

可以先看一段代码,让两个线程同时让对count 进行相加100次

package CAS;
​
public class casDemo {
    public static int count=0;
    public static void main(String[] args) {
        //创建两个线程同时加上 count
        for (int i = 0; i <2 ; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    for (int j = 0; j < 100; j++) {                    
                            count++;
                        System.out.println(count);
                    }
​
                }
            }).start();
​
        }
        System.out.println(count);
    }
}

最后运行一下,发现count的结果一定不是200,这是因为这段代码是非线程安全的,所以最终的自增结果可能会小于200,那么我们加上锁试一下(synchronized

package CAS;
​
public class casDemo {
    public static int count=0;
    public static void main(String[] args) {
        //创建两个线程同时加上 count
        for (int i = 0; i <2 ; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    for (int j = 0; j < 100; j++) {
                        //加上锁
                        synchronized (casDemo.class){
                            count++;
                        }
​
                        System.out.println(count);
                    }
​
                }
            }).start();
​
        }
        System.out.println(count);
    }
}

加了同步锁之后,count自增的操作变成了原子性操作,所以最终输出一定是count=200,代码实现了线程安全。

虽然synchronized确保了线程安全,但是在于性能上这就不是很好的选择。

synchronized关键字会让没有得到锁资源的线程进入BLOCKED状态,而后在争夺到锁资源后恢复为RUNNABLE状态,这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高。

使用java中的“原子操作类”

所谓原子操作类,指的是java.util.concurrent.atomic包下,一系列以Atomic开头的包装类。如AtomicBoolean,AtomicUInteger,AtomicLong。它们分别用于Boolean,Integer,Long类型的原子性操作。

public class casDemo {
    //把count包装一下
    public static AtomicInteger count=new AtomicInteger(0);
    public static void main(String[] args) {
        //创建两个线程同时加上 count
        for (int i = 0; i <2 ; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    for (int j = 0; j < 100; j++) {
                        //实现count增加
                        count.incrementAndGet();
                        System.out.println(count);
                    }
​
                }
            }).start();
​
        }
        System.out.println(count);
    }
}

使用AtomicInteger之后,最终的输出结果同样可以保证是200。并且在某些情况下,代码的性能会比synchronized更好。

Atomic操作类的底层正是用到了“CAS机制”。

synchronized和CAS的区别

从思想上来看,synchronized是属于悲观锁,悲观的认为程序中的并发情况严重,会严防死守,CAS是属于乐观锁,乐观的认为程序中 的并发情况不那么严重,所以让线程不断去重试更新

CAS的缺点

  • CPU开销过大: 在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很到的压力。
  • 不能保证代码块的原子性: CAS机制所保证的知识一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized了。
  • ABA问题:这是CAS机制最大的问题所在。

JAVA中CAS的底层实现

1、什么是unsafe

什么是unsafe呢?Java语言不像C,C++那样可以直接访问底层操作系统,但是JVM为我们提供了一个后门,这个后门就是unsafe。unsafe为我们提供了硬件级别的原子操作。

CAS是一种原子操作。那么Java是怎样来使用CAS的呢?

我们知道,在Java中,如果一个方法是native的,那Java就不负责具体实现它,而是交给底层的JVM使用c或者c++去实现。

Unsafe类是JDK提供的一个不安全的类,它提供了一些底层的操作,包括内存操作、线程调度、对象实例化等。它的作用是让Java可以在底层直接操作内存,从而提高程序的效率。但是,由于Unsafe类是不安全的,所以只有JDK开发人员才能使用它,普通开发者不建议使用。它里面大多是一些native方法,其中就有几个关于CAS的:

boolean compareAndSwapObject(Object o, long offset,Object expected, Object x);
boolean compareAndSwapInt(Object o, long offset,int expected,int x);
boolean compareAndSwapLong(Object o, long offset,long expected,long x);
  • 调用compareAndSwapInt、compareAndSwapLong或compareAndSwapObject方法时,会传入三个参数,分别是需要修改的变量V、期望的值A和新值B。

  • 方法会先读取变量V的当前值,如果当前值等于期望的值A,则使用新值B来更新变量V,否则不做任何操作。

  • 方法会返回更新操作是否成功的标志,如果更新成功,则返回true,否则返回false。

    由于CAS操作是基于底层硬件支持的原子性指令来实现的,所以它可以保证操作的原子性和线程安全性,同时也可以避免使用锁带来的性能开销。因此,CAS操作广泛应用于并发编程中,比如实现无锁数据结构、实现线程安全的计数器等。

2、原子操作类解析

看一下AtomicInteger当中常用的自增方法incrementAndGet:

public final int incrementAndGet() {
​
  for (;;) {
​
   int current = get();
​
   int next = current + 1;
​
   if (compareAndSet(current, next))
​
     return next;
​
  }
​
}
​
private volatile int value; 
​
public final int get() {
​
  return value;
​
}

这段代码是一个无限循环,也就是CAS的自旋(底层为do-while循环),循环体中做了三件事:

  1. 获取当前值
  2. 当前值+1,计算出目标值
  3. 进行CAS操作,如果成功则跳出循环,如果失败则重复上述步骤

这里需要注意的重点是get方法这个方法的作用是获取变量的当前值。

如何保证获取的当前值是内存中的最新值?

很简单,用volatile关键字来保证(保证线程间的可见性)。

我们接下来在看一下compareAndSet方法的实现:

public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

compareAndSet方法的实现很简单,只有一行代码。这里涉及到两个重要的对象,一个是unsafe,一个是valueOffset。 unsafe上面提到,就不用多说了,对于valueOffset对象,是通过unsafe.objectFiledOffset方法得到,所代表的是AtomicInteger对象value成员变量在内存中的偏移量。我们可以简单的把valueOffset理解为value变量的内存地址

我们上面说过,CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。

而unsafe的compareAndSwapInt方法的参数包括了这三个基本元素:valueOffset参数代表了V,expect参数代表了A,update参数代表了B。

正是unsafe的compareAndSwapInt方法保证了Compare和Swap操作之间的原子性操作。

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);

3、ABA问题

什么是ABA问题?

ABA问题指在CAS操作过程中,当一个值从A变成B,又更新回A,普通CAS机制会误判通过检测。这时候就可能导致程序出现意外的结果。

在高并发场景下,使用CAS操作可能存在ABA问题,也就是在一个值被修改之前,先被其他线程修改为另外的值,然后再被修改回原值,此时CAS操作会认为这个值没有被修改过,导致数据不一致。

如何解决?

为了解决ABA问题,Java中提供了AtomicStampedReference类(原子标记参考),该类通过使用版本号的方式来解决ABA问题。每个共享变量都会关联一个版本号,CAS操作时需要同时检查值和版本号是否匹配。因此,如果共享变量的值被改变了,版本号也会发生变化,即使共享变量被改回原来的值,版本号也不同,因此CAS操作会失败。

4、CPU空转

为什么出现CPU空转?

除了ABA问题,CAS操作还可能会受到自旋时间过长的影响,因为如果某个线程一直在自旋等待,会浪费CPU资源。

解决方法

可以采用自适应自旋锁的方式,即在前几次重试时采用忙等待的方式,后面则使用阻塞等待的方式,避免浪费CPU资源。

5、总结

1、java语言CAS底层如何实现?

利用unsafe提供的原子性操作方法。

2.什么事ABA问题?怎么解决?

当一个值从A变成B,又更新回A,普通CAS机制会误判通过检测。

利用版本号比较可以有效解决ABA问题。

应用场景

主要在并发编程的应用中非常的广泛,通常用于实现乐观锁和无锁算法

  • 线程安全计数器:由于CAS操作是原子性的,因此CAS可以用来实现一个线程安全的计数器;
  • 队列: 在并发编程中,队列经常用于多线程之间的数据交换。使用CAS可以实现无锁的非阻塞队列(Lock-Free Queue);
  • 数据库并发控制: 乐观锁就是通过CAS实现的,它可以在数据库并发控制中保证多个事务同时访问同一数据时的一致性;
  • 自旋锁: 自旋锁是一种非阻塞锁,当线程尝试获取锁时,如果锁已经被其他线程占用,则线程不会进入休眠,而是一直在自旋等待锁的释放。自旋锁的实现可以使用CAS操作;
  • 线程池: 在多线程编程中,线程池可以提高线程的使用效率。使用CAS操作可以避免对线程池的加锁,从而提高线程池的并发性能。