CAS及源码解析

629 阅读5分钟

1、为什么要用CAS,它解决什么问题?

CAS相当于没有加锁,多个线程都可以直接操作共享资源,实际去修改的时候才去判断能否修改成功。有时候效率会比Syn高,比如对一个值进行累加操作,JUC包下的Atomic类就行。

问题

针对n++

public class Case {
​
    public volatile int n;
​
    public void add() {
        n++;
    }
}

add方法的字节码

public void add();
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0       
         1: dup           
         2: getfield      #2                  // Field n:I
         5: iconst_1      
         6: iadd          
         7: putfield      #2                  // Field n:I
        10: return        

可以发现add方法的n++操作被拆分成三个字节码指令,尽管用volatile能保证线程之间的可见性,但是无法保证三个指令是原子执行的,在多线程情况下无法实现线程安全

解决方案:

synchronized

public class Case {
​
    public volatile int n;
​
    public synchronized void add() {
        n++;
    }
}

synchronized可行但是性能差一些,我们考虑能否进行优化

public int a = 1;
public boolean compareAndSwapInt(int b) {
    if (a == 1) {
        a = b;
        return true;
    }
    return false;
}

思想很好,但是实操不行,如果线程1和线程2都判断成功,都会对a进行修改。

我们可以通过对compareAndSwapInt进行加锁实现同步,从而把他变成原子操作,也就是CAS方案

2、实现原理:(compare and swap)

基本原理是类似一个死循环,先获取当前值,计算期望值,然后调用CAS方法进行更新,如果此时当前值等于期望值,则进行更新操作;否则表明值被更改,再去取最新值并尝试更新值直到成功为止。当判断的过程中,然后另外一个线程更改这个值了怎么办?CAS是原子性操作,cpu指令级别的支持,不会被中途打断。

CAS(V,Expected,NewValue)
if (V == E){
    this.V = NewValue;
    return true;
}else{
    return false;
}
expected  = read m;
cas(0,1){
    for(;;)如果当前m值==0,认定没有线程进来改变它的值,m=1
}otherwise try again or fail

底层实现原理是通过Unsafe的三个本地方法实现的,以AtomicInteger类的getAndAdd(int delta)为例子

public class AtomicInteger extends Number implements java.io.Serializable {
    // 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 int get() {return value;}
}

1、Unsafe是CAS的核心类,通过本地方法进行访问

2、valueOffset为变量值在内存中的偏移地址,unsafe通过偏移地址来得到数据的原值的。

3、value当前值,使用volatile修饰,保证多线程环境下看见的是同一个。

 //getAndAdd源码
 public final int getAndAdd(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta);
    }
​
//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;
    }
​
//compareAndSwapInt源码
//这里var1代表当前AtomicInteger对象,var2表示实例的内存地址偏移量,v5代表预期的旧值。v5代表需要更新的值
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

举例:

假设线程A和线程B同时执行getAndAdd操作:

1、AtomicInteger里面的value原值为A,那么根据JMM,线程1和线程2各自持有一个value副本为1;

2、线程1通过通过getIntVolatile获得当前内存值为A,此时因为上下文切换被挂起(主要模拟多线程下的场景)

3、线程2通过getIntVolatile获得当前内存值为A,碰巧未被挂起,继续执行compareAndSwapInt方法比较当前值和线程中的预期值,发现相等,说明内存值没有被其他线程提前修改过,修改内存值为B

4、此时线程1执行compareAndSwapInt方法比较,发现内存值为B和自己线程内的预期值不一样,说明该值已经被其他线程更改过了

5、线程1循环执行getIntVolatile再次获得内存值,因为内存值被volatile修饰,所以其他线程对他的修改,线程A总能看到,然后线程A继续执行compareAndSwapInt进行比较替换,直到成功。

原理小结:当执行原子类的方法时,底层是通过Unsafe类三个本地方法实现的,unsafe通过valueOffset的偏移地址获得数据的原值,然后用volatile修饰value以保证多线程环境下内存可见性。做完这些准备后执行原子类的某个方法,比如addAndGet,他通过unsafe的getAndAddInt方法,返回一个int类型数据,通过后面CompareAndSwapInt中返回var5的含义可知返回的是修改值。调用getAndAddInt方法时,底层调用一个do,while循环,里面执行compareAndSwapInt方法,而这个方法是本地方法,就是一个死循环,比较期望值和当前值是否相等,相等的时候循环结束后返回修改值

3、缺点

ABA问题

比如说一个线程1从内存位置V中取出A,这时候另一个线程2也从内存中取出A,并且2进行了一些操作变成了B,然后2又将V位置的数据变成A,这时候线程1进行CAS操作发现内存中仍然是A,然后1操作成功。 尽管线程1的CAS操作成功,但是不代表这个过程就是没有问题的。

int类型其实无所谓,如果是Object类,是个引用,如果真的要关注,要加版本号,A指向B,B指向C,如果一个线程b先改成A指向C,改变C,然后又改成指向B,现在B是否能指向C就不好说了因为C发生了改变。和女友分手后复合,中间经历了别的女人

解决办法就是使用AtomicStampedReference类,说白了就是加个版本,对比的就是内存加版本是否一致,从而保证CAS的正确性

参考资料

Java编程的逻辑

www.jianshu.com/p/fb6e91b01…

objcoding.com/2018/11/29/…

www.cmsblogs.com/article/139…

segmentfault.com/a/119000001…

\