Java并发突击-CAS

129 阅读5分钟

在我们平时开发工作中,只要遇到并发相关的,首先会想到原子操作,哦,不对是道格·利,紧接着是AtomicInteger,AtomicLong,LongAccumulator,LongAdder等一堆原子类直冲天灵盖,hold down~~ hold down~~ ,我们今天不讲这些,今天我们讲讲他们的父亲,哦,不,是二大爷CAS.

什么是CAS

CAS(Compare And Swap 比较并交换),CAS本身是一种思想,一种无锁算法,可以看做是乐观锁的一种实现方式。

通俗解释:我要修改一个值V,我期望V的值是6,如果我从内存取出来的值确实为6,与我期望的一致,那我就把他修改为8;如果取出来的不是6,那我就认为他被别人修改过,与我期望的不一致,那我就放弃对他的修改。

从上边的例子我们可以看出CAS有三个操作数:

  • 内存值V
  • 预期值6
  • 要修改的值8

当且仅当预期值6与内存值V相同时,才会将内存值修改为8,否则什么都不做,并放回现在的值V

image.png

下面我们通过一段代码再来加深一下对这个概念的理解:

package com.yqj.juc.jmm;  
  
/**  
 * CAS 原理演示  
 * @author Zhao Yun Long  
 * @version V1.0  
 * @date 2022/11/02 15:06  
 */public class CasDemo implements Runnable {  
  
    private volatile int value;  
  
    public synchronized int compareAndSwap(int expectedValue, int newValue) {  
        int oldValue = value;  
        if (oldValue == expectedValue){  
            value = newValue;  
            System.out.println("线程" + Thread.currentThread().getName() + "修改成功");  
        }else {  
            System.out.println("线程" + Thread.currentThread().getName() + "修改失败");  
        }  
        return oldValue;  
    }  
  
    public static void main(String[] args) throws InterruptedException {  
        CasDemo casDemo = new CasDemo();  
        casDemo.value = 0;  
        Thread t1 = new Thread(casDemo,"t1");  
        Thread t2 = new Thread(casDemo,"t2");  
        t1.start();  
        t2.start();  
        t1.join();  
        t1.join();  
    }  
  
    @Override  
    public void run() {  
        compareAndSwap(0,1);  
    }  
}

Pasted image 20221102184419.png 运行结果第一个线程修改成功后,第二个线程compare为false,修改失败。

CAS应用场景

其实在JUC包下很多原子类(我们开篇提到了部分)都是通过CAS来实现的,我们就以AtomicInteger为例来看下:

package java.util.concurrent.atomic;  
import java.util.function.IntUnaryOperator;  
import java.util.function.IntBinaryOperator;  
import sun.misc.Unsafe;  
   
public class AtomicInteger extends Number implements java.io.Serializable {  
    private static final long serialVersionUID = 6214790243416807050L;  
  
    // setup to use Unsafe.compareAndSwapInt for updates  
    private static final Unsafe unsafe = Unsafe.getUnsafe();  
    private static final long valueOffset;  
  
    static {  
        try {  
            valueOffset = unsafe.objectFieldOffset  
                (AtomicInteger.class.getDeclaredField("value"));  
        } catch (Exception ex) { throw new Error(ex); }  
    }  
  
    private volatile int value;  
	/**
	* 省略若干代码。。。。。。。。。。
	*/

    public final boolean compareAndSet(int expect, int update) {  
	    //来,看这儿---------@-↓↓↓↓↓↓↓-@---------
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);  
        //来,看这儿---------@-↑↑↑↑↑↑↑-@---------
    } 

	/**
	* 省略若干代码。。。。。。。。。。
	*/
}

我们能看到compareAndSet(int expect, int update) 方法调用了unsafe类中的compareAndSwapInt(this, valueOffset, expect, update),这一步暂时到这儿,忽略compareAndSwapInt方法的前两个参数, expect, update这两个参数我们看名字也能猜个大概,expect对应的就是上边我们自己实现的CasDemo类中的expectedValue,update对应的就是newValue。到这一步也验证了AtomicInteger是通过CAS实现的,我们再继续往深了走,方便我们更深的理解CAS.

CAS源码分析

上边我们跟到了unsafe.compareAndSwapInt(this, valueOffset, expect, update),我们继续点进去可以看到一下代码:

Pasted image 20221102185156.png 我们可以看到有三个类似的方法:

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

在 Java 中,CAS 操作是由 Unsafe 类提供支持的,该类定义了以上三种针对不同类型变量的 CAS 操作。

从图中我们也能看出,Unsafe类最终调用的是native方法,底层其实是JVM通过C++调用处理器的指令cmpxchg来实现的。这里我们以linux_64x的为例,查看Atomic::cmpxchg的实现:

#atomic_linux_x86.inline.hpp 
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) { 
//判断当前执行环境是否为多处理器环境 
int mp = os::is_MP(); 
//LOCK_IF_MP(%4) 在多处理器环境下,为 cmpxchgl 指令添加 lock 前缀,以达到内存屏障的效果 
//cmpxchgl 指令是包含在 x86 架构及 IA-64 架构中的一个原子条件指令, 
//它会首先比较 dest 指针指向的内存值是否和 compare_value 的值相等, 
//如果相等,则双向交换 dest 与 exchange_value,否则就单方面地将 dest 指向的内存值交给exchange_value。 
//这条指令完成了整个 CAS 操作,因此它也被称为 CAS 指令。
__asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)" 
				  : "=a" (exchange_value) 
				  : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp) 
				  : "cc", "memory"); 
return exchange_value;

源码看不懂没关系,我们只需要知道CAS 指令作为一种硬件原语,有着天然的原子性,这也正是 CAS 的价值所在。

那人无完人,同样CAS也是有天生缺陷的,他虽然高效地解决了原子操作,但是还是存在一些缺陷的:

  • 自旋 CAS 长时间地不成功,则会给 CPU 带来非常大的开销
  • 只能保证一个共享变量原子操作
  • ABA 问题

什么是ABA问题

假设我们原来的值为6,通过操作更新成了8,经过其他业务处理后又变成了6,这时候我们CAS去检查的时候发现值是6,他会认为这个值没变过,然后就把他更新了,但其实这个值已经被变过了。 针对这个问题,我们可以增加版本号,每次修改的时候,版本号累加,就类似数据库中的乐观锁。 同样,Java也提供了相应的原子引用类AtomicStampedReference:

public class AtomicStampedReference<V> {  
  
    private static class Pair<T> {  
        final T reference;  
        final int stamp;  
        private Pair(T reference, int stamp) {  
            this.reference = reference;  
            this.stamp = stamp;  
        }  
        static <T> Pair<T> of(T reference, int stamp) {  
            return new Pair<T>(reference, stamp);  
        }  
    }  
  
    private volatile Pair<V> pair;

reference即我们实际存储的变量stamp是版本,每次修改可以通过+1保证版本唯一性。这样就可以保证每次修改后的版本也会往上递增。

关于CAS的突击就到这儿啦,文章有错误的地方请评论区指正。