来聊聊CAS

写在文章开头

你好,我叫sharkchili,目前还是在一线奋斗的Java开发,经历过很多有意思的项目,也写过很多有意思的文章,是CSDN Java领域的博客专家,也是Java Guide的维护者之一,非常欢迎你关注我的公众号:写代码的SharkChili,这里面会有笔者精心挑选的并发、JVM、MySQL数据库专栏,也有笔者日常分享的硬核技术小文。

在这里插入图片描述

什么是CAS

CAS全称Compare-And-Swap ,是一种无锁编程算法 ,即比较当前的值与旧值是否相等若相等则进行修改操作(乐观锁机制),该类常用于多线程共享变量的修改操作。而其底层实现也是基于硬件平台的汇编指令,JVM只是封装其调用仅此而已。而本文会基于以下大纲展开对CAS的探讨。

CAS基础使用示例

如下所示,可以看出使用封装CAS操作的AtomicInteger操作多线程共享变量无需我们手动加锁,因为避免过多人为操作这就大大减少了多线程操作下的失误。

使用原子类操作共享数据

csharp 复制代码
public class CasTest {
    private AtomicInteger count = new AtomicInteger();


    public void increment() {
        count.incrementAndGet();
    }
    // 使用 AtomicInteger 后,不需要加锁,也可以实现线程安全
    public int getCount() {
        return count.get();
    }

    public static void main(String[] args) {

    }
}

使用sync锁操作数据

arduino 复制代码
public class Test {
    private int i=0;
    public synchronized int add(){
        return i++;
    }
}

从源码角度了解java如何封装汇编的UNSAFE

代码也很简单,就是拿到具有可见性的volatile变量i,然后判断i和当前对象paramObject对应的i值是否一致,若一致则说明没被人该过,进而进行修改操作,反之自旋循环获取在进行CAS。

arduino 复制代码
public final int getAndAddInt(Object paramObject, long paramLong, int paramInt)
  {
    int i;
    do
      i = getIntVolatile(paramObject, paramLong);
    while (!compareAndSwapInt(paramObject, paramLong, i, i + paramInt));
    return i;
  }

  public final long getAndAddLong(Object paramObject, long paramLong1, long paramLong2)
  {
    long l;
    do
      l = getLongVolatile(paramObject, paramLong1);
    while (!compareAndSwapLong(paramObject, paramLong1, l, l + paramLong2));
    return l;
  }

  public final int getAndSetInt(Object paramObject, long paramLong, int paramInt)
  {
    int i;
    do
      i = getIntVolatile(paramObject, paramLong);
    while (!compareAndSwapInt(paramObject, paramLong, i, paramInt));
    return i;
  }

  public final long getAndSetLong(Object paramObject, long paramLong1, long paramLong2)
  {
    long l;
    do
      l = getLongVolatile(paramObject, paramLong1);
    while (!compareAndSwapLong(paramObject, paramLong1, l, paramLong2));
    return l;
  }

  public final Object getAndSetObject(Object paramObject1, long paramLong, Object paramObject2)
  {
    Object localObject;
    do
      localObject = getObjectVolatile(paramObject1, paramLong);
    while (!compareAndSwapObject(paramObject1, paramLong, localObject, paramObject2));
    return localObject;
  }

手写Unsafe实现20个线程500次CAS自增

代码逻辑和注释如下,读者可自行debug查看逻辑

csharp 复制代码
public class CasCountInc {

    private static Logger logger = LoggerFactory.getLogger(CasCountInc.class);

    // 获取Unsafe对象
    private static Unsafe unsafe = getUnsafe();

    // 线程池数目
    private static final int THREAD_COUNT = 20;

    // 每个线程运行自增次数
    private static final int EVERY_THREAD_ADD_COUNT = 500;

    // 自增的count的值,volatile保证可见性
    private volatile int count = 0;

    // count字段的偏移量
    private static long countOffSet;

    private static Unsafe getUnsafe() {
        Unsafe unsafe = null;
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);
        } catch (Exception e) {
            logger.info("获取unsafe失败,失败原因:[{}]", e.getMessage(), e);
        }
        return unsafe;
    }


    static {
        try {
            countOffSet = unsafe.objectFieldOffset(CasCountInc.class.getDeclaredField("count"));
        } catch (NoSuchFieldException e) {
            logger.error("获取count的偏移量报错,错误原因:[{}]", e.getMessage(), e);
        }
    }


    public void inc() {
        int oldCount = 0;
        //基于cas完成自增
        do {
            oldCount = count;
        } while (!unsafe.compareAndSwapInt(this, countOffSet, oldCount, oldCount + 1));
    }


    public static void main(String[] args) throws InterruptedException {
        CasCountInc casCountInc = new CasCountInc();
        CountDownLatch countDownLatch = new CountDownLatch(THREAD_COUNT);
        IntStream.range(0, THREAD_COUNT).forEach(i -> {
            new Thread(() -> {
                IntStream.range(0, EVERY_THREAD_ADD_COUNT).forEach((j) -> {
                    casCountInc.inc();
                });
                countDownLatch.countDown();
            }).start();
        });

        countDownLatch.await();

        logger.info("count最终结果为 [{}]", casCountInc.count);
    }
}

原子类简介

原子类更新基本类型

makefile 复制代码
AtomicBoolean: 原子更新布尔类型。
AtomicInteger: 原子更新整型。
AtomicLong: 原子更新长整型。

原子类更新数组类型

makefile 复制代码
AtomicIntegerArray: 原子更新整型数组里的元素。
AtomicLongArray: 原子更新长整型数组里的元素。
AtomicReferenceArray: 原子更新引用类型数组里的元素。

基本使用示例

java 复制代码
import java.util.concurrent.atomic.AtomicIntegerArray;

public class AtomicIntegerArrayDemo {


    public static void main(String[] args) throws InterruptedException {
        AtomicIntegerArray array = new AtomicIntegerArray(new int[] { 0, 0 });
        System.out.println(array);
//        索引1位置+2
        System.out.println(array.getAndAdd(1, 2));
        System.out.println(array);
    }
}

原子类更新引用类型

makefile 复制代码
AtomicReference: 原子更新引用类型。

AtomicStampedReference: 原子更新引用类型, 内部使用Pair来存储元素值及其版本号。

AtomicMarkableReferce: 原子更新带有标记位的引用类型。

原子类操作引用类型使用示例

csharp 复制代码
import java.util.concurrent.atomic.AtomicReference;

public class AtomicReferenceTest {

    public static void main(String[] args){

        // 创建两个Person对象,它们的id分别是101和102。
        Person p1 = new Person(101);
        Person p2 = new Person(102);
        // 新建AtomicReference对象,初始化它的值为p1对象
        AtomicReference ar = new AtomicReference(p1);
        // 通过CAS设置ar。如果ar的值为p1的话,则将其设置为p2。
        ar.compareAndSet(p1, p2);

        Person p3 = (Person)ar.get();
        System.out.println("p3 is "+p3);
        System.out.println("p3.equals(p1)="+p3.equals(p1));
        System.out.println("p3.equals(p2)="+p3.equals(p2));
    }
}



class Person {
    volatile long id;
    public Person(long id) {
        this.id = id;
    }
    public String toString() {
        return "id:"+id;
    }
}

原子类更新成员变量

方法简介

makefile 复制代码
AtomicIntegerFieldUpdater: 原子更新整型的字段的更新器。
AtomicLongFieldUpdater: 原子更新长整型字段的更新器。
AtomicStampedFieldUpdater: 原子更新带有版本号的引用类型。
AtomicReferenceFieldUpdater: 上面已经说过此处不在赘述。

使用示例

如下所示,我们创建一个基础类DataDemo,通过原子类CAS操作字段值进行自增操作。

java 复制代码
public class TestAtomicIntegerFieldUpdater {


    private static Logger logger = LoggerFactory.getLogger(TestAtomicIntegerFieldUpdater.class);

    public static void main(String[] args) {
        TestAtomicIntegerFieldUpdater tIA = new TestAtomicIntegerFieldUpdater();
        tIA.doIt();
    }

    /**
     * 返回需要更新的整型字段更新器
     *
     * @param fieldName
     * @return
     */
    public AtomicIntegerFieldUpdater<DataDemo> updater(String fieldName) {
        return AtomicIntegerFieldUpdater.newUpdater(DataDemo.class, fieldName);
    }

    public void doIt() {
        DataDemo data = new DataDemo();
        // 修改公共变量,返回更新前的旧值 0
        AtomicIntegerFieldUpdater<DataDemo> updater = updater("publicVar");
        int oldVal = updater.getAndIncrement(data);
        logger.info("publicVar 更新前的值[{}] 更新后的值 [{}]", oldVal, data.publicVar);


        // 更新保护级别的变量
        AtomicIntegerFieldUpdater<DataDemo> protectedVarUpdater = updater("protectedVar");
        int oldProtectedVar = protectedVarUpdater.getAndAdd(data, 2);
        logger.info("protectedVar 更新前的值[{}] 更新后的值 [{}]", oldProtectedVar, data.protectedVar);


        // logger.info("privateVar = "+updater("privateVar").getAndAdd(data,2)); 私有变量会报错

        /*
         * 下面报异常:must be integer
         * */
//        logger.info("integerVar = "+updater("integerVar").getAndIncrement(data));
        //logger.info("longVar = "+updater("longVar").getAndIncrement(data));
    }


    class DataDemo {

        // 公共且可见的publicVar
        public volatile int publicVar = 0;
        // 保护级别的protectedVar
        protected volatile int protectedVar = 4;
        // 私有变量
        private volatile int privateVar = 5;

        // final 不可变量
        public final int finalVar = 11;

        public volatile Integer integerVar = 19;
        public volatile Long longVar = 18L;

    }

}

通过上述代码我们可以总结出CAS字段必须符合以下要求:

markdown 复制代码
1. 变量必须使用volatile保证可见性
2. 必须是当前对象可以访问到的类型才可进行操作'
3. 只能是实例变量而不是类变量,即不可以有static修饰符
4. 包装类也不行

CAS的ABA问题

CAS更新前会检查值有没有变化,如果没有变化则认为没人修改过,在进行更新操作。这种情况下,若我们A值修改为B,B再还原为A。这种修改再还原的操作,CAS是无法感知是否变化的,这就是所谓的ABA问题。

AtomicStampedReference源码详解

源码如下所示,可以看到AtomicStampedReference解决ABA问题的方式是基于当前修改操作的时间戳和元引用值是否一致,若一直则进行数据更新

arduino 复制代码
public class AtomicStampedReference<V> {
    private static class Pair<T> {
        final T reference;  //维护对象引用
        final int stamp;  //用于标志版本
        private Pair(T reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
        static <T> Pair<T> of(T reference, int stamp) {
            return new Pair<T>(reference, stamp);
        }
    }
    private volatile Pair<V> pair;
    ....
    
    /**
      * 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 &&
           //可以看到这个括号里面用了一个短路运算如果当前版本与新值一样就说更新过,就不往下走CAS代码了
            ((newReference == current.reference &&
           
            newStamp == current.stamp) ||
            // 构造新的Pair对象并CAS更新
            casPair(current, Pair.of(newReference, newStamp)));
    }

    private boolean casPair(Pair<V> cmp, Pair<V> val) {
        // 调用Unsafe的compareAndSwapObject()方法CAS更新pair的引用为新引用
        return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
    }

AtomicStampedReference解决ABA问题示例

代码示例,我们下面就用other代码模拟干扰现场,如果other现场先进行CAS更新再还原操作,那么main线程的版本号就会过时,CAS就会操作失败

csharp 复制代码
/**
 * ABA问题代码示例
 */
public class AtomicStampedReferenceTest {
    private static AtomicStampedReference<Integer> atomicStampedRef =
            new AtomicStampedReference<>(1, 0);

    public static void main(String[] args) {
        Thread main = new Thread(() -> {
            System.out.println("操作线程" + Thread.currentThread() + ",初始值 a = " + atomicStampedRef.getReference());
            int stamp = atomicStampedRef.getStamp(); //获取当前标识别
            try {
                Thread.sleep(1000); //等待1秒 ,以便让干扰线程执行
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean isCASSuccess = atomicStampedRef.compareAndSet(1, 2, stamp, stamp + 1);  //此时expectedReference未发生改变,但是stamp已经被修改了,所以CAS失败
            System.out.println("操作线程" + Thread.currentThread() + ",CAS操作结果: " + isCASSuccess);
        }, "主操作线程");

        Thread other = new Thread(() -> {
            Thread.yield(); // 确保thread-main 优先执行
            atomicStampedRef.compareAndSet(1, 2, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
            System.out.println("操作线程" + Thread.currentThread() + ",【increment】 ,值 = " + atomicStampedRef.getReference());
            atomicStampedRef.compareAndSet(2, 1, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
            System.out.println("操作线程" + Thread.currentThread() + ",【decrement】 ,值 = " + atomicStampedRef.getReference());
        }, "干扰线程");

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

}

AtomicMarkableReference解决对象ABA问题

AtomicMarkableReference,它不是维护一个版本号,而是维护一个boolean类型的标记,标记对象是否有修改,从而解决ABA问题。

typescript 复制代码
public boolean weakCompareAndSet(V       expectedReference,
                                     V       newReference,
                                     boolean expectedMark,
                                     boolean newMark) {
        return compareAndSet(expectedReference, newReference,
                             expectedMark, newMark);
    }

常见面试题

CAS为什么比synchronized快(重点)

CAS工作原理是基于乐观锁且操作是原子性的,与synchronized的悲观锁(底层需要调用操作系统的mutex锁)相比,效率也会相对高一些。

CAS是不是操作系统执行的?(重点)

不是,CAS是主要是通过处理器的指令来保证原子性的。

CAS存在那些问题?

但即便如此CAS仍然存在两个问题:

  1. 可能存在长时间CAS:如下代码所示,这就是AtomicInteger底层的UNSAFE类如何进行CAS的具体代码 ,可以看出这个CAS操作需要拿到volatile变量后在进行循环CAS才有可能成功这就很可能存在自旋循环,从而给CPU带来很大的执行开销。
arduino 复制代码
 public final int getAndSetInt(Object var1, long var2, int var4) {
        int var5;//声明一个var5,只
        do {
         //通过getIntVolatile获取当前原子数的值
            var5 = this.getIntVolatile(var1, var2);
            //只有传入var1和现在获得的var5 一样,才说明值没被修改过,我们才能将值设置为var4
        } while(!this.compareAndSwapInt(var1, var2, var5, var4));

        return var5;
    }
  1. CAS只能对一个变量进行原子操作:为了解决这个问题,JDK 1.5之后通过AtomicReference使得变量可以封装成一个对象进行操作
  2. ABA问题:总所周知CAS就是比对当前值与旧值是否相等,在进行修改操作,假设我现在有一个变量值为A,我改为B,再还原为A,这样操作变量值是没变的?那么CAS也会成功不就不合理吗?这就好比一个银行储户想查询概念转账记录,如果转账一次记为1,如果按照ABA问题的逻辑,那么这个银行账户转账记录次数有可能会缺少。为了解决这个问题JDK 1.5提供了AtomicStampedReference,通过比对版本号在进行CAS操作,那么上述操作就会变为1A->2B->3A,由于版本追加,那么我们就能捕捉到当前变量的变化了。

AtomicInteger自增到10000后如何归零

ini 复制代码
AtomicInteger atomicInteger=new AtomicInteger(10000);
atomicInteger.compareAndSet(10000, 0);

CAS 平时怎么用的,会有什么问题,为什么快,如果我用 for 循环代替 CAS 执行效率是一样的吗?(重点)

CAS 平时怎么用的:我们希望从一个视频中完成人像比对的功能,通过图像识别技术完成视频逐帧切割,只要有一帧的图片被识别分数达到90以上即可算完成。任务是串行的,如果是个大视频则执行时间会非常漫长。

经过对需求复盘,整体来说这个功能就是多任务只要有一个任务完成就算完成的需求。对此我们写了一个Callable的任务,这个任务中的多线程共享两个变量atomicIntegercountDownLatch,都是由外部调度的,只要一个任务分数达到90则CAS自增,countDown倒计时门闩。

java 复制代码
public class Task implements Callable<Integer> {

    private static Logger logger = LoggerFactory.getLogger(Task.class);

    private AtomicInteger atomicInteger ;


    private CountDownLatch countDownLatch;

    public Task(AtomicInteger atomicInteger, CountDownLatch countDownLatch) {
        this.atomicInteger = atomicInteger;
        this.countDownLatch = countDownLatch;
    }

    @Override
    public Integer call() throws Exception {

        int score = (int) (Math.random() * 100);
        logger.info("当前线程:{},识别分数:{}", Thread.currentThread().getName(), score);
        synchronized (this){

        }
        if (score > 90 && atomicInteger.getAndIncrement()==0) {
                logger.info("当前线程:{} countDown",Thread.currentThread().getName());
                countDownLatch.countDown();
                logger.info("当前线程:{} 返回比对分数:{}", Thread.currentThread().getName(), score);
                return score;
        }


        return -1;
    }
}

执行代码

ini 复制代码
public class Main {
    
    private static Logger logger = LoggerFactory.getLogger(Main.class);

    public static void main(String[] args) throws InterruptedException {
        ExecutorService threadPool = Executors.newFixedThreadPool(100);
        CountDownLatch countDownLatch=new CountDownLatch(1);

        for (int i = 0; i < 100; i++) {
            Future<Integer> task = threadPool.submit(new Task(countDownLatch));
        }


        logger.info("阻塞中");
        countDownLatch.await();
        logger.info("阻塞结束");

        threadPool.shutdown();

        while (!threadPool.isTerminated()){
            
        }


        
    }
}

存在问题:CAS是基于乐观锁机制,所以数据同步失败就会原地自旋,在高并发场景下开销很大,所以线程数很大的情况下不建议使用原子类。

存在问题: 如果并发量大的话,自旋的线程多了就会导致性能瓶颈。 for 循环代替 CAS执行效率是否一样:大概率是CAS快,原因如下:

  1. CAS是native方法更接近底层
  2. for循环为了保证线程安全可能会用到sync锁或者Lock无论那种都需要上锁和释放的逻辑,相比CAS乐观锁来说开销很大。

小结

我是sharkchiliCSDN Java 领域博客专家开源项目---JavaGuide contributor ,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili ,同时我的公众号也有我精心整理的并发编程JVMMySQL数据库个人专栏导航。

参考资料

JUC原子类: CAS, Unsafe和原子类详解

深入理解高并发编程

本文使用 markdown.com.cn 排版

相关推荐
MacroZheng7 分钟前
Redis官方发布高颜值可视化工具,功能更是强的离谱!
java·redis·后端
回家路上绕了弯25 分钟前
为什么 AI 写代码时,总喜欢“防御性编程”?
后端
灯澜忆梦28 分钟前
【Redis中间件】#4 | 进阶数据类型语法
redis·后端
广州山泉婚姻31 分钟前
前后端分离的核心优势
前端·后端
IT_陈寒1 小时前
Vue的响应式让我加班到凌晨,问题竟出在这个不起眼的地方
前端·人工智能·后端
乱码三千1 小时前
使用 Docker-compose 搭建SRS直播中转服务端
后端·架构·github
y1su1 小时前
【Leetcode】1477. 找两个和为目标值且不重叠的子数组
数据结构·后端·算法·leetcode·职场和发展
愛芳芳1 小时前
Swagger2 多环境动态配置实战指南
java·后端·maven·springboot·swagger
JaguarJack2 小时前
PHP 8.6 只是个小版本,三十项弃用却已在为 PHP 9.0 铺路
后端·php·服务端
BingoGo2 小时前
PHP 8.6 只是个小版本,三十项弃用却已在为 PHP 9.0 铺路
后端·php