专栏首页琦小虾的BinaryJava并发技术总结之四——CAS

Java并发技术总结之四——CAS

接上篇《Java并发技术总结之三——线程状态》

四. CAS 原理

参考地址: 《JAVA并发编程: CAS和AQS》 《面试必问的CAS,你懂了吗?》

CAS (Compare And Swap),即比较并交换,是解决多线程并行情况下使用锁造成性能损耗的一种机制。在 JAVA 中,sun.misc.Unsafe 类提供了硬件级别的原子操作来实现 CAS。 java.util.concurrent 包下的大量类 (AtomicInteger, AtomicBoolean, AtomicLong, … )都使用了这个 Unsafe.java 类的 CAS 操作。至于 Unsafe.java 的具体实现这里就不讨论了。 CAS 本身是一种乐观锁的实现方式,在 Java 中的运用很多:

  1. AQS:作为 ReentrantLock, CountDownLatch 等锁的底层基础;
    • 它的乐观锁实现方式,体现在入队时反复执行 CAS 操作,直到 CAS 操作成功,即入队成功;
    • CAS 与 AQS 的关系与区别
      • CAS 是一种解决并发问题的思想,也就是先比较后替换,JUC 通过自旋执行 CAS 操作实现线程安全的状态更新。
      • AQS 是 Java 并发包的一个底层框架,是可重入锁 (ReentrantLock) 与共享锁(比如 CountDownLatch, CyclicBarrier 等)的基础。关于 Lock 与 AQS,Lock 面向用户,AQS 面向 Lock,也就是说 AQS 为各种 Lock 提供了底层的支持,AQS 的最核心原理之一就是利用 CAS 更新同步状态。
  2. Atomit 类:一系列原子类,底层都是使用 CAS 操作;
  3. ConcurrentHashMap:1.7 -> 1.8 的优化,使用 CAS 替代了 1.7 版本的 ReentrantLock;

下面以 AtomicInteger.java 的部分实现来大致讲解下这些原子类的实现。

注:CAS 与 AQS 的关系与区别: CAS 是一种解决并发问题的思想,也就是先比较后替换,JUC 通过自旋执行 CAS 操作实现线程安全的状态更新。 AQS 是 Java 并发包的一个底层框架,是可重入锁 (ReentrantLock) 与共享锁(比如 CountDownLatch, CyclicBarrier 等)的基础。关于 Lock 与 AQS,Lock 面向用户,AQS 面向 Lock,也就是说 AQS 为各种 Lock 提供了底层的支持,AQS 的最核心原理之一就是利用 CAS 更新同步状态。

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();

    // 初始 int 大小
    private volatile int value;
    // 省略了部分代码...

    // 返回旧值,并设置新值为 newValue
    public final int getAndSet(int newValue) {
        /**
         * 这里使用 for 循环不断通过 CAS 操作来设置新值
         * CAS 实现和加锁实现的关系有点类似乐观锁和悲观锁的关系
         */
        for (;;) {
            int current = get();
            if (compareAndSet(current, newValue))
                return current;
        }
    }

    // 原子的设置新值为 update, expect 为期望的当前的值
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

    // 获取当前值 current,并设置新值为 current+1
    public final int getAndIncrement() {
        for (;;) {
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
                return current;
        }
    }

    // 此处省略部分代码,余下的代码大致实现原理都是类似的
}

核心系列方法 compareAndSet 包含三个操作数——内存位置 (V)、预期原值 (A) 和新值 (B)。CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则不做任何更改,只告诉我这个位置现在的值即可。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。

在竞争不是特别激烈的时候,使用该包下的原子操作性能比使用 synchronized 关键字的方式高效的多(查看 getAndSet(int newValue) 源码,可知如果资源竞争十分激烈的话,这个 for 循环可能换持续很久都不能成功跳出。不过这种情况更应该考虑降低资源竞争)。

注:通常使用 AtomicInteger,会用到它的 getAndIncrement() 方法作计数器。

CAS 最主要的运用,就是在 JUC 中的 AbstractQueuedSynchronizer,即 AQS,它是 Java 中多种锁实现的父类。该类核心同步队列的入队操作是一种乐观锁实现,多线程情况下对头节点、尾节点操作都有可能失效,失效后 CAS 会再次尝试,直到尝试成功。比如 AbstractQueuedSynchronizer # enq(Node node) 方法:

    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

此外,CAS 仍然有三个缺点:

  1. 循环时间开销大
    • getAndAddInt() 源码中可以看到,如果 CAS 执行返回 false,那么会一直执行尝试,如果 CAS 长时间不成功,可能会有比较大的 CPU 资源开销;
  2. 只保证一个共享变量原子操作
    • 对于多个共享变量,无法使用 CAS 方式保证操作原子性,此时应该使用锁 (synchronized, Lock) 保证原子性;
  3. ABA 问题
    • ABA 问题:如果内存地址 V 上的值在读取时为 A,准备赋值的时候检查它的值也为 A,但也不能保证在这期间没有被其他线程改变过
    • 在使用 CAS 之前要考虑好一致性的效果,是需要达到最终一致性,还是完全一致性。如果需要解决 ABA 问题,Java 中提供了 AtomicStampedReference / AtomicMarkableReference 来处理会发生 ABA 问题的场景,它们的主要思想是在对象中额外再增加一个版本号的标记,标识对象是否有过变更;此外也可以改用传统互斥同步。
  4. 不适用于高并发场景

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

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Ubuntu环境如何上传项目到GitHub网站?

    Ubuntu环境如何上传项目到GitHub网站? 之前笔者写了一篇《CMake学习笔记(三)——以笔者的Robosub竞赛为例》的博客。博客中笔者以自己的项目为...

    剑影啸清寒
  • 程序生成之编译、链接、加载浅析

    程序生成之编译、链接、加载浅析 最近笔者看论文烦得慌,便又重新拾起之前没有完全完成的交叉编译,准备在网上找资料,好好研究一下。 讲道理,笔者其实对编译链接的...

    剑影啸清寒
  • Ubuntu 16.04 LTS 的 Sublime Text 3 安装及中文配置

    版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.n...

    剑影啸清寒
  • 乐观锁的缺点

    如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可...

    崔笑颜
  • 锋利的jQuery第八期

    上一期介绍了属性过滤选择器,提到了正则,那这次还是稍微说说吧,首先是不等于,开头以及结尾。原效果图如下:

    聚沙成塔
  • Git想撤销commit?和(non-fast-forward)的问题

    1、git pull origin master --allow-unrelated-histories //把远程仓库和本地同步,消除差异

    瑞新
  • 【GNN】Diff Pool:网络图的层次化表达

    今天学习的是斯坦福大学的同学 2018 年的工作《Hierarchical Graph Representation Learning with Differe...

    阿泽 Crz
  • 7.12 Git 工具 - 打包

    虽然我们已经了解了网络传输 Git 数据的常用方法(如 HTTP,SSH 等),但还有另外一种不太常见却又十分有用的方式。

    shaonbean
  • 三分钟理解“外观模式”——设计模式轻松掌握

    实际生活中的例子: 现在流行炒股,股民一般都手持好多个股票,而股民每天需要关注手中N个股票的动向,随时针对不同的股票作出不同的决策,这样感觉心好累;于是有的人选...

    大闲人柴毛毛
  • Spark内核分析之Spark的HA源码分析

            Spark作业运行的集群环境有两种,分别基于standalone模式和Yarn集群模式。我们知道Yarn集群提供了HA来保证了集群的高可用,而s...

    z小赵

扫码关注云+社区

领取腾讯云代金券