Java并发之CAS原理入门

介绍

通常情况下,共享变量在并发中的处理是使用synchronized关键字或lock

// 共享变量
static int count = 0;

// 并发方法
public static synchronized boolean request() {
    count++;
}

但是这种方式效率过低,是否存在一种更高效的处理方式:在修改共享变量之前,判断期望值和当前值,如果不一致则一直等待,如果相等那么修改共享变量。新的方式只需要在判断逻辑中加锁

// volatile保证可见性,避免拿到缓存
volatile static int count = 0;

/**
* 判断是否更新
* @param expectCount 期望值count
* @param newCount 赋新值
* @return 是否成功
*/
public static synchronized boolean compareAndSwap(int expectCount, int newCount) {
    // 判断当前count和期望值是否一致
    if (getCount() == expectCount) {
        count = newCount;
        return true;
    }
    return false;
}

public static int getCount() {
    return count;
}

public static void request(){
    int expectCount;
    // 判断和赋值
    while(!compareAndSwap((expectCount=getCount()),expectCount+1)){}
}

CAS

原理

CAS全称是CompareAndSwap,比较并替换

CAS需要有3个操作数:内存地址V,旧的预期值A,即将要更新的目标值B
CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作

CAS是通过JNI借助C语言实现的,例如指令cmpxchg

系统底层进行CAS操作时,会判断当前系统是否是多核心系统,会给总线加锁,然后执行CAS操作

CAS的问题:高并发情况下存在性能问题

ABA

如果存在以下的情况:
并发1:获取出数据的初始值是A,后续计划实施CAS,期望数据还是A,新值为B
并发2:将数据修改成B再修改回A
并发1:CAS检测发现被修改后的数据是A,修改为B

或者可以理解为丢失了一个版本更新

简单地模拟

public static AtomicInteger a = new AtomicInteger(1);

public static void main(String[] args) {
    Thread main = new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("操作线程:"+Thread.currentThread().getName());
            System.out.println("初始值:"+a.get());
            try {
                int expectNum = a.get();
                int newNum = expectNum+1;
                Thread.sleep(1000);
                boolean isCASSuccess = a.compareAndSet(expectNum,newNum);
                System.out.println("操作线程:"+Thread.currentThread().getName());
                System.out.println("CAS是否成功:"+isCASSuccess);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    },"主线程");
    Thread other = new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                // 确保主线程优先执行
                Thread.sleep(20);
                // a++
                a.incrementAndGet();
                System.out.println("操作线程:"+Thread.currentThread().getName());
                System.out.println("increment:"+a.get());
                // a--
                a.decrementAndGet();
                System.out.println("操作线程:"+Thread.currentThread().getName());
                System.out.println("decrement:"+a.get());
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    },"干扰线程");

    main.start();
    other.start();
}

打印如下,主线程并没有感受到这个值的变化

操作线程:主线程
初始值:1
操作线程:干扰线程
increment:2
操作线程:干扰线程
decrement:1
操作线程:主线程
CAS是否成功:true

问题解决:

  1. 业务对数据敏感时,避免使用CAS
  2. 记录修改版本号判断,JDK中已有实现(AtomicStampedReference)

对上述案例的解决

// 第一个参数是数据,第二个参数是初始化版本
public static AtomicStampedReference<Integer> a = new AtomicStampedReference<>(1, 1);

public static void main(String[] args) {
    Thread main = new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("操作线程:" + Thread.currentThread().getName());
            System.out.println("初始值:" + a.getReference());
            try {
                Integer expectReference = a.getReference();
                Integer newReference = expectReference + 1;

                int expectStamp = a.getStamp();
                int newStamp = expectStamp + 1;

                Thread.sleep(1000);
                boolean isCASSuccess = a.compareAndSet(expectReference, newReference, expectStamp, newStamp);
                System.out.println("操作线程:" + Thread.currentThread().getName());
                System.out.println("CAS是否成功:" + isCASSuccess);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }, "主线程");
    Thread other = new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                // 确保主线程优先执行
                Thread.sleep(20);
                // a++
                a.compareAndSet(a.getReference(), a.getReference() + 1, a.getStamp(), a.getStamp() + 1);
                System.out.println("操作线程:" + Thread.currentThread().getName());
                System.out.println("increment:" + a.getReference());
                // a--
                a.compareAndSet(a.getReference(), a.getReference() - 1, a.getStamp(), a.getStamp() + 1);
                System.out.println("操作线程:" + Thread.currentThread().getName());
                System.out.println("decrement:" + a.getReference());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }, "干扰线程");

    main.start();
    other.start();
}

成功解决

操作线程:主线程
初始值:1
操作线程:干扰线程
increment:2
操作线程:干扰线程
decrement:1
操作线程:主线程
CAS是否成功:false

源码

JDK在sun.misc.unsafe类中实现

  1. public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
  2. public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
  3. public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

var1:表示要操作的对象(比较并替换的对象)
var2:表示要操作对象中属性地址的偏移量
var4:修改数据的期望值
var5:需要修为的新值