java CAS详解(深入源码剖析)

CAS是什么

CAS是compare and swap的缩写,即我们所说的比较交换。该操作的作用就是保证数据一致性、操作原子性。

cas是一种基于锁的操作,而且是乐观锁。在java中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加version来获取数据,性能较悲观锁有很大的提高。

CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和A的值是一样的,那么就将内存里面的值更新成B。否则将不做任何处理。

CAS是通过无限循环来获取数据的,若果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才有可能机会执行。

为什么说CAS能很好的保证数据一致性,因为它是直接从硬件层面保证了原子性。

CAS是一条CPU的原子指令(cmpxchg指令),Unsafe提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg。

执行cmpxchg指令的时候,会判断当前系统是否为多核系统,如果是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功之后会执行cas操作,也就是说CAS的原子性实际上是CPU实现独占的。比起用synchronized重量级锁, 这里的排他时间要短很多, 所以在多线程情况下性能会比较好。

测试案例讲解

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;

public class TestCas {
    public static int count = 0;
    private final static int MAX_TREAD=10;
    public static AtomicInteger atomicInteger = new AtomicInteger(0);
    public static void main(String[] args) throws InterruptedException {
        /*CountDownLatch能够使一个线程在等待另外一些线程完成各自工作之后,再继续执行。
        使用一个计数器进行实现。计数器初始值为线程的数量。当每一个线程完成自己任务后,计数器的值就会减一。
        当计数器的值为0时,表示所有的线程都已经完成一些任务,然后在CountDownLatch上等待的线程就可以恢复执行接下来的任务。*/
        CountDownLatch latch = new CountDownLatch(MAX_TREAD);

        //匿名内部类
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 1000; i++) {
                    count++;
                    atomicInteger.getAndIncrement();
                }
                latch.countDown(); // 当前线程调用此方法,则计数减一
            }
        };
        //同时启动多个线程
        for (int i = 0; i < MAX_TREAD; i++) {
            new Thread(runnable).start();
        }
        latch.await(); // 阻塞当前线程,直到计数器的值为0
        System.out.println("理论结果:" + 1000 * MAX_TREAD);
        System.out.println("static count: " + count);
        System.out.println("AtomicInteger: " + atomicInteger.intValue());
    }
}

我们发现每次运行,atomicInteger 的结果值都是正确的,count++的结果却不对
在这里插入图片描述

为什么AtomicInteger类的操作能保证数据一致性呢个?进入它的源码:

public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

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;
}
//cas方法
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

可以看到最终是调用了Unsafe类中的compareAndSwapInt,该方法就是CAS方法其中的一个。Unsafe类中总共有三个涉及CAS的方法

/*
@param o 包含要修改的字段的对象
@param offset 字段在对象内的偏移量
@param expected 期望值(旧的值)
@param update 更新值(新的值)
@return true 更新成功 | false 更新失败
*/
public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object update);
public final native boolean compareAndSwapInt( Object o, long offset, int expected,int update);
public final native boolean compareAndSwapLong( Object o, long offset, long expected, long update);

在执行 Unsafe 的 CAS 方法的时候,这些方法首先将内存位置的值与预期值(旧的值)比较,如果相匹配,那么处理器会自动将该内存位置的值更新为新值,并返回 true ;如果不相匹配,处理器不做任何操作,并返回 false 。

总的来说,AtomicInteger类主要利用CAS (compare and swap) + volatile和 native方法来保证原子操作。

通过看上述getAndAddInt方法源码,可见如果cas失败会不断循环重复尝试。这就常说的cas自旋,所谓自旋其实是重复调用compareAndSwap方法,涉及到的方法有以下几个,也是在Unsafe类中。

  • getAndAddInt
  • getAndAddLong
  • getAndSetInt
  • getAndSetLong
  • getAndSetObject

CAS缺点

  1. ABA问题。因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。

  2. 循环时间长开销大。高并发下N多线程同时去操作一个变量,会造成大量线程CAS失败,然后处于自旋状态,导致严重浪费CPU资源,降低了并发性。

ABA问题解决方案(AtomicStampedReference)

JDK 的提供了一个类似 AtomicStampedReference 类来解决 ABA 问题。

AtomicStampReference 在 CAS 的基础上增加了一个 Stamp 整型 印戳(或标记),使用这个印戳可以来觉察数据是否发生变化,给数据带上了一种实效性的检验。
AtomicStampReference 的 compareAndSet 方法首先检查当前的对象引用值是否等于预期引用,并且当前印戳( Stamp )标志是否等于预期标志,如果全部相等,则以原子方式将引用值和印戳( Stamp )标志的值更新为给定的更新值。

  1. AtomicStampReference 的构造器

    /**  
    * @param initialRef初始引用  
    * @param initialStamp初始戳记  
    */
    AtomicStampedReference(V initialRef, int initialStamp)
    
  2. AtomicStampReference的核心方法

    AtomicStampReference类中有自己的compareAndSet方法,进入源码:

    /**
    * expectedReference 引用的旧值
    * newReference 引用的新值
    * expectedStamp 旧的戳记
    * newStamp 新的戳记 
    */
    public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
        Pair<V> current = pair;
        return
            expectedReference == current.reference &&
            expectedStamp == current.stamp &&
            ((newReference == current.reference &&
              newStamp == current.stamp) ||
             casPair(current, Pair.of(newReference, newStamp))); //unsafe类中的cas
    }
    

    可以看到旧值expectedReference和旧印戳expectedStamp都会进行比较,都满足才会调用Unsafe中的cas方法。

你可能感兴趣的:(并发编程,Java,java,CAS,并发编程)