并发之CAS算法的技术原理

CAS是对一种处理器指令(例如x86处理器中的cmpxchg指令)的称呼。 不少多线程相关的Java标准库类的实现最终都会借助CAS。虽然在实际工作中 多数情况下我们并不需要直接使用CAS, 但是理解CAS有助于我们更好地理解相关标准库类, 以便恰当地使用它们。保障像自增这种比较简单的操作的原子性我们有更好的选择一CAS算法。

CAS全称Compare And Swap(比较与替换),是一种"无锁"算法。在不使用java.util.concurrent包下的原子类就是通过CAS来实现乐观锁。

CAS算法:总共由三个操作数,一个内存值v,一个线程本地内存旧值a(期望操作前的值)和一个新值b,在操作期间先拿旧值a和内存值v比较有没有发生变化,如果没有发生变化,才能内存值v更新成新值b,发生了变化则不交换。循环CAS算法则是不停的执行CAS操作。一般情况下,“更新”是一个不断重试的操作。

由偏向锁→轻量级锁→自旋锁→重量级锁。 其中轻量级锁采用的就是类似于cas算法来实现的。


并发之CAS算法的技术原理_第1张图片

根据定义我们可以看出各属性的作用:

unsafe: 获取并操作内存的数据。
valueOffset: 存储value在AtomicInteger中的偏移量。
value: 存储AtomicInteger的int值,该属性需要借助volatile关键字保证其在线程间是可见的。接下来,我们查看AtomicInteger的自增函数incrementAndGet()的源码时,发现自增函数底层调用的是unsafe.getAndAddInt()。

// AtomicInteger 自增方法
public final int incrementAndGet() {
  return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
// Unsafe.class
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;
}

// ------------------------- OpenJDK 8 -------------------------
// Unsafe.java
public final int getAndAddInt(Object o, long offset, int delta) {
   int v;
   do {
       v = getIntVolatile(o, offset);
   } while (!compareAndSwapInt(o, offset, v, v + delta));
   return v;
}

源码我们可以看出,getAndAddInt()循环获取给定对象o中的偏移量处的值v,然后判断内存值是否等于v。如果相等则将内存值设置为 v + delta,否则返回false,继续循环进行重试,直到设置成功才能退出循环,并且将旧值返回。整个“比较+更新”操作封装在compareAndSwapInt()中,在JNI里是借助于一个CPU指令完成的,属于原子操作,可以保证多个线程都能够看到同一个变量的修改值。

后续JDK通过CPU的cmpxchg指令,去比较寄存器中的 A 和 内存中的值 V。如果相等,就把要写入的新值 B 存入内存中。如果不相等,就将内存值 V 赋值给寄存器中的值 A。然后通过Java代码中的while循环再次调用cmpxchg指令进行重试,直到设置成功为止。

CAS虽然很高效,但是它也存在三大问题,这里也简单说一下:

ABA问题。CAS需要在操作值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。但是如果内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行检查时会发现值没有发生变化,但是实际上是有变化的。ABA问题的解决思路就是在变量前面添加版本号,每次变量更新的时候都把版本号加一,这样变化过程就从“A-B-A”变成了“1A-2B-3A”。

JDK从1.5开始提供了AtomicStampedReference类来解决ABA问题,具体操作封装在compareAndSet()中。compareAndSet()首先检查当前引用和当前标志与预期引用和预期标志是否相等,如果都相等,则以原子方式将引用值和标志的值设置为给定的更新值。

如果当前引用 == 预期引用,并且当前标志等于预期标志,则以原子方式将该引用和该标志的值设置为给定的更新值。其中参数代表:expectedReference - 该引用的预期值;newReference - 该引用的新值;
expectedStamp - 该标志的预期值;newStamp - 该标志的新值。

循环时间长开销大。CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销。如果JVM能支持处理器提供pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。

只能保证一个共享变量的原子操作。对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的。

Java从1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。

然后自己也模拟了一个:

/**
 *     模拟cas算法
 */
public class TestCompareAndSwap {

    public static void main(String[] args) {
        final CompareAndSwap cas=new CompareAndSwap();

        for(int i=0;i<10;i++){

            new Thread(new Runnable() {
                @Override
                public void run() {
                    int expectValue=cas.get();
                    boolean b = cas.compareAndSet(expectValue, (int) (Math.random() * 10));
                    System.out.println(b);
                }
            }).start();
        }
    }
}

 class CompareAndSwap{

    private int value;

    //获取内存值
    public synchronized int get(){
        return value;
    }

    //比较
    public  synchronized int compareAndSwap(int expectedValue,int newValue){
        int oldValue=value;

        if(oldValue == expectedValue){
            this.value=newValue;
        }
        return oldValue;
    }

    //设置
    public synchronized  boolean compareAndSet(int expectedValue,int newValue){
        return expectedValue==compareAndSwap(expectedValue,newValue);
    }
}

整理不易,喜欢请点赞

你可能感兴趣的:(并发之CAS算法的技术原理)