并发编程中的CAS

在Java中,锁在鬓发处理中占据了一席之地,但是使用锁有一个不好的地方,就是当一个线程获取不到锁时会被阻塞挂起,这会导致线程上下文的切换和重新调度开销。Java提供了非阻塞的volatile关键字来解决共享变量的可见性问题,这在一定程度上弥补了锁带来的开销问题,但是volatile只能保证共享变量的可见性,不能解决读——改——写等的原子性问题。CAS即Compare and Swap,是JDK提供的非阻塞原子性操作,他通过硬件保证了比较——更新操作的原子性。

一、Unsafe类

上面提到了CAS是通过硬件保证了比较——更新操作的原子性。那么它是怎么通过硬件来保证的呢?

在Java中提供了很多原子操作类,比如java.util.concurrent.atomic包下的类,比如AtomInteger,跟随AtomInteger的代码我们一路往下,就能发现最终调用的是 sum.misc.Unsafe 这个类。

JDK的rt.jar包中的Unsafe类提供了硬件级别的原子操作,Unsafe类中的方法都是native方法,它们使用JNI的方式访问本地C++实现库。下面我们来了解一下Unsafe提供的几个重要的方法。

  • long objectFieldOffset(Field var1)方法:返回指定的变量在所属类中的内存偏移地址。如下代码使用Unsafe类获取变量value在AtomInteger对象中的内存偏移地址。
 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;
  • boolean compareAndSwapInt(Object obj, long offset, int except, int update)方法:比较对象(obj)中偏移量为offset的变量的值是否与except相等,如果相等,则将这个变量更新成update,返回true,否则返回false。

二、什么是CAS

CAS: 全称Compare and swap,字面意思:”比较并交换“,并发的情况下会用到CAS,一个 CAS 涉及到以下操作:

我们假设内存中的原数据V,旧的预期值A,更新后的值B。

1、 比较 A 与 V 是否相等。
2、(比较) 如果比较相等,将 B 写入 V。(交换)
3、 返回操作是否成功。

举一个例子:假设我们有一个变量value,初始值为A,现在需要将它更新成B,有两个线程同时执行这一操作。假设线程 I 先执行,如果线程 I 通过CAS操作发现value的值仍然为A,没有被其他线程更改过,那么成功的将value值改为B。接下来线程 II 执行CAS操作同样将value的值由A改为B,发现value的值已经不等于A了,那么就操作失败。

2.1、原子操作类的应用

在Java中提供了很多原子操作类,比如AtomicInteger,其中有一个自增方法,看它是如何使用CAS的:


 private static final long valueOffset;

    static {
      try {
      //获取变量value的偏移量地址
        valueOffset = unsafe.objectFieldOffset
            (AtomicInteger.class.getDeclaredField("value"));
      } catch (Exception ex) { throw new Error(ex); }
    }
    
private volatile int value;
 
  public AtomicInteger(int initialValue) {
  //这种初始化下value等于传入的值
        value = initialValue;
    }

    public AtomicInteger() {
    //这种初始化方式下 value默认为0
    }
    
	public final int get() {
        return value;
    }
    
 //自增方法
 public final int incrementAndGet() {
      //循环,如果compareAndSet(current, next)=false就一直循环,相当于自旋,知道成功为止
        for (;;) {
        //获取当前的value值
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
                return next;
        }
    }

/**
 * this:当前对象 AtomicInteger
 */
 public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

2.2 CAS实现原子操作的三大问题

  • ABA问题:加入线程 I 使用CAS修改初始值为A的变量X,那么线程 I 首先去获取当前变量X的值(为A),然后使用CAS操作尝试修改X的值为B,如果使用CAS操作成功了,那么程序运行一定是正确的吗?其实未必,这是因为有可能线程 I 在获取变量X的值A后,在执行CAS前,线程 II 使用CAS修改了变量X的值为B,然后又修改了变量X的值为A。虽然线程 I 执行CAS时X的值为A,但是这个A已经不是线程 I 获取时的A了。这就是ABA问题。

ABA问题的产生是应为变量的状态值产生了环形转换,只要不构成环形就会解决问题,解决的思路就是使用版本号,在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A——B——A就会变成1A——2B——3A。从Java1.5开始,JDk的Atomic包里面就提供了一个类AtomicStampedReference来解决这个问题,这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子的方式将该引用和该标志的值设置为给定的更新值。

  • 循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来很大的执行开销。

  • 只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可使用循环CAS的方式保证原子操作,但是对多个共享变量进行操作时,循环CAS无法保证多个原子操作之间的原子性。此时可以用到锁。

三、总结

文章详细讲解了CAS的原理,CAS可以进行原子更新一个值(包括对象),主要用于读多写少的场景,如原子自增操作,如果多线程调用,在CAS失败之后,会死循环一直重试,直到更新成功。这种方式很消耗CPU资源,虽然没有锁,但循环的自旋可能比锁的代价还高。同时存在ABA问题,但AtomicStampedReference通过加入版本号机制已经解决。而且只能保证一个共享变量的原子操作,所以在多个共享变量操作时需要使用锁来解决。CAS在很多地方都有使用,比如J.U.C(java.util.concurrent)包中大量使用了CAS,ConcurrentHashMap也使用到,因此CAS极其重要。

发布了6 篇原创文章 · 获赞 5 · 访问量 976

猜你喜欢

转载自blog.csdn.net/qq_28203555/article/details/104067608