什么是CAS?CAS有什么缺点?

什么是CAS

CAS 的全称是 Compare And Swap 即比较交换,其算法核心思想如下函数:CAS(V,E,N) 参数:

  1. V 表示要更新的变量
  2. E 预期值
  3. N 新值

如果 V 值等于 E 值,则将 V 的值设为 N。若 V 值和 E 值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。

通俗的理解就是 CAS 操作需要我们提供一个期望值,当期望值与当前线程的变量值相同时,说明还没线程修改该值,当前线程可以进行修改,也就是执行 CAS 操作,但如果期望值与当前线程不符,则说明该值已被其他线程修改,此时不执行更新操作,但可以选择重新读取该变量再尝试再次修改该变量,也可以放弃操作。

我们看一下例子:

  1. 在内存地址V当中,存储这值为10的变量

在这里插入图片描述
2. 此时线程1想要把变量的值增加1。对于线程1来说,旧的预期值为E=10,要修改的值 N=11。
在这里插入图片描述
3. 线程1要提交更新之前,另一个线程2抢先一步,把内存地址V的变量值先更改成了11

在这里插入图片描述
4. 线程1开始提交更新,首先进行E和内存V中实际值比较,发现E不等于V的实际值,提交失败。

在这里插入图片描述
5. 线程1重新获取内存地址V的当前值,并重新计算想要修改的新值,此时对线程1来说,E=11,N=12。这个重新尝试的过程被称为自旋。

在这里插入图片描述
6. 这一次没有发现其他线程改变地址V的值。线程1进行Compare ,发现N和地址V的实际值是相等的。

在这里插入图片描述
7.线程1进行swap,把地址V的值替换为N ,也就是12.

在这里插入图片描述

CAS 举例说明


public static int count = 0;

public static void main(String[] args) {
    
    
  //
    for (int i=0;i<5;i++){
    
    
        new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    

                try{
    
    
                    Thread.sleep(1000);
                }catch (Exception e){
    
    }

                //每个线程当中让count值自增100次
                for(int j=0;j< 10000;j++){
    
    
                    count++;
                }

            }
        }).start();
    }
    try{
    
    
        Thread.sleep(2000);
    }catch (Exception e){
    
    }

  System.out.println("count=="+count);
}

如上示例程序,因为上面代码不是线程安全的,所以最终的结果可能会小于 50000。

那么怎么解决呢?可以加锁(synchronized)。

 public static int count = 0;

public static void main(String[] args) {
    
    
  //
    for (int i=0;i<5;i++){
    
    
        new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    

                try{
    
    
                    Thread.sleep(1000);
                }catch (Exception e){
    
    }

                //每个线程当中让count值自增100次
                for(int j=0;j< 10000;j++){
    
    
                    synchronized (Demo1.class) {
    
    
                        count++;
                    }
                }

            }
        }).start();
    }
    try{
    
    
        Thread.sleep(2000);
    }catch (Exception e){
    
    }

  System.out.println("count=="+count);
}

加了同步锁以后,count自增的操作变成了原子性操作,所以最终的输出一定是count = 50000。

Synchronized 的确保证了线程安全,但是在某些情况下,却不是一个最优选择。

为什么这么说?关键在于性能问题

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

尽管jdk1.6 为Synchronized做了优化,增加了从偏向锁到轻量级锁再到重量级锁的过度,但是在最终转变为重量级锁之后,性能仍然较低

java原子操作类,指的是java.util.concurrent.atomic包下,一些列以Atomic开头的包装类。例如AtomicBoolean、AtomicInteger、AtomicLong。他们分别用于boolean、Integer、Long类型的原子性操作。

现在我们尝试在代码中引入**AtomicInteger **类:

 public static AtomicInteger count = new AtomicInteger(0);

public static void main(String[] args) {
    
    
  //
    for (int i=0;i<5;i++){
    
    
        new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    

                try{
    
    
                    Thread.sleep(1000);
                }catch (Exception e){
    
    }

                //每个线程当中让count值自增100次
                for(int j=0;j< 10000;j++){
    
    
                    synchronized (Demo1.class) {
    
    
                        count.incrementAndGet();
                    }
                }

            }
        }).start();
    }
    try{
    
    
        Thread.sleep(2000);
    }catch (Exception e){
    
    }

  System.out.println("count=="+count.get());
}

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

Atomic 操作类的底层,正是利用了CAS机制;

CAS 底层实现

下面看一下AtomicInteger的源代码

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的自旋。循环体当中做了三件事:

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

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

如何保证获得的当前值时内存中的最新值呢?很简单,用volatile关键字来保证,

可是compareAndSet方法是如何保证原子性操作的呢??

接下来看一看compareAndSet方法的实现,以及方法所依赖对象的来历:
在这里插入图片描述
compareAndSet方法的实现很简单,只有一行代码。这里涉及到两个重要的对象,一个是unsafe,一个是valueOffset

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

至于valueOffset对象,是通过unsafe.objectFieldOffset方法得到,所代表的是AtomicInteger对象value成员变量在内存中的偏移量。我们可以简单地把valueOffset理解为value变量的内存地址。

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

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

CAS缺陷

1.ABA 问题

因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。
ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A。举个通俗点的例子,你倒了一杯水放桌子上,干了点别的事,然后同事把你水喝了又给你重新倒了一杯水,你回来看水还在,拿起来就喝,如果你不管水中间被人喝过,只关心水还在,这就是ABA问题。
如果你是一个讲卫生讲文明的小伙子,不但关心水在不在,还要在你离开的时候水被人动过没有,因为你是程序员,所以就想起了放了张纸在旁边,写上初始值0,别人喝水前麻烦先做个累加才能喝水。

2.循环时间长 开销大

自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销

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

当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子
性,这个时候就可以用锁。还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java 1.5开始,JDK提供了AtomicReference类来保证引用对之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作

猜你喜欢

转载自blog.csdn.net/fd2025/article/details/119570426#comments_22658093