CAS机制是什么鬼?

本文公众号来源:Felix周

作者:Felix zyf

概述

比较并交换(compare and swap, CAS),是原子操作的一种,可用于在多线程编程中实现不被打断的数据交换操作,从而避免多线程同时改写某一数据时由于执行顺序不确定性以及中断的不可预知性产生的数据不一致问题。该操作通过将内存中的值与指定数据进行比较,当数值一样时将内存中的数据替换为新的值。

大家都知道悲观锁、乐观锁吧?这里的悲观锁、乐观锁指的是线程方面的锁,不是数据库方面的锁。其实CAS机制就是乐观锁。synchronized操作就是一种悲观锁,这种线程一旦得到锁,其他需要锁的线程就挂起,等待持有锁的线程释放锁。当一个线程正在等待锁时,它不能做任何事,所以悲观锁有很大的缺点。

乐观锁的核心思路就是每次不加锁而是假设修改数据之前其他线程一定不会修改,如果因为修改过产生冲突就失败就重试,直到成功为止。CAS机制就是一种。可以理解成一个无阻塞多线程争抢资源的模型

CAS 操作包含三个操作数 —— 内存地址(V)、旧的预期值(A)和即将要更新的新值(B)。执行CAS操作的时候,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。

画个图演示下吧

理解CAS的核心就是:CAS是原子性的,虽然你可能看到比较后再修改(compare and swap)觉得会有两个操作,但终究是原子性的!

++、--问题

volatile关键字大家很眼熟吧?HashMap源码、双重检测机制实现单例等场景都出现过它的身影。这里我就不详细介绍 volatile关键字,因为要说的太多了,说到 volatile就要说到JMM,说到JMM就要说原子性、有序性、一致性,然后又涉及到处理器优化、指令重排等等,可以扯个几小时。其实这也是为什么面试官喜欢问HashMap的问题,因为可以深度挖掘的东西太多了,可以直接看出面试者的深浅。好了,扯远了,回归文章。。。

volatile关键字主要是保证能 禁止指令重排序和保证 可见性,但是要注意,这并不能保证 原子性

看代码:

public class Cases {
    public static volatile int n;

    public static void add() {
        n++;
    }

    public static void main(String[] args) throws InterruptedException {

        ExecutorService service = Executors.newCachedThreadPool();
        // 20个线程对共享变量进行add()
        for (int i = 0; i < 20; i++) {
            service.execute(() -> {
                for (int j = 0; j < 1000; j++) {
                    add();
                }
            });
        }
        // 等待上述的线程执行完
        service.shutdown();
        service.awaitTermination(1, TimeUnit.DAYS);

        System.out.println(n);
    }

}

这段代码创建20个线程,每个线程对n变量进行1000次自增,如果这段代码能够正确并发的话,最后输出的结果应该是20000。但是,运行结果每次都是一个小于20000的数字。为什么呢?

问题就出现在 n++这个自增操作上面。我们用Javap反编译:cmd里面进入该类路径,输入 javacCases.java编译成class文件后,再输入指令 javap-verboseCases,查看add方法的字节码指令 贴出部分:

n++被拆分成了几个指令:

  1. 执行getfield拿到原始n;
  2. 执行iadd进行加1操作;
  3. 执行putfield写把累加后的值写回n;

通过 volatile修饰的变量可以保证线程之间的可见性,但 n++这个操作并不是一个指令操作,而是三个, volatile并不能保证这3个指令的原子执行,因此在多线程并发执行下,无法做到线程安全。

那么怎么解决呢?大家都知道,在add方法加上synchronized修饰就可以解决。这个方案自然也可以,都是是否有一种杀鸡用牛刀的感觉,就一个简单的自增操作,就要用synchronized来锁起来,性能上差了好多。

JDK原子变量类简单介绍

看下几个JDK自带的CAS方案:

注:JDK8新增DoubleAccumulator、LongAccumulator、DoubleAdder、LongAdder,是对AtomicLong等类的改进。比如LongAccumulator与LongAdder在高并发环境下比AtomicLong更高效。

原子变量类使用

使用原子变量类更改之前的案例

public class Cases {
    // 共享变量(使用AtomicInteger来替代Synchronized锁)
    public static AtomicInteger count = new AtomicInteger(0);

    public static void add() {
        count.incrementAndGet();
    }

    public static void main(String[] args) throws InterruptedException {

        ExecutorService service = Executors.newCachedThreadPool();
        // 20个线程对共享变量进行add()
        for (int i = 0; i < 20; i++) {
            service.execute(() -> {
                for (int j = 0; j < 1000; j++) {
                    add();
                }
            });
        }
        // 等待上述的线程执行完
        service.shutdown();
        service.awaitTermination(1, TimeUnit.DAYS);

        System.out.println(count);
    }

}

修改完,无论执行多少次,我们的结果永远是20000!

其余几个原子变量类就不演示了,使用方法差不多,可以自行看看API,百度。

CAS缺陷

循环时间长开销大

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

只能保证一个共享变量的原子操作

当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。

ABA问题

如果内存地址V初次读取的值是A,并且在准备赋值的时候检查到它的值仍然为A,那我们就能说它的值没有被其他线程改变过了吗?

如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA”问题。

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

因此,在使用CAS前要考虑清楚“ABA”问题是否会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更高效。

本文分享自微信公众号 - Java3y(java3y)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-11-05

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏丑胖侠

JVM之内存结构详解

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 ...

8640
来自专栏CNN

【Ubuntu】Tensorflow对训练后的模型做8位(uint8)量化转换

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 ...

17430
来自专栏丑胖侠

Java8 Stream新特性详解及实战

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 ...

9750
来自专栏丑胖侠

Java8新特性之空指针异常的克星Optional类

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 ...

6620
来自专栏丑胖侠

Java8 Stream性能如何及评测工具推荐

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 ...

11030
来自专栏丑胖侠

Java8新特性时间日期库DateTime API及示例

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 ...

17740
来自专栏丑胖侠

wildfly(JBoss AS)应用服务器快速入门

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 ...

13030
来自专栏码客

maven常用的jar

8430
来自专栏丑胖侠

Java8 Lambda表达式详解手册及实例

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 ...

10630
来自专栏丑胖侠

Java SPI机制实战详解及源码分析

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 ...

8720

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励