Java多线程学习笔记 - 十三、CAS原理

本文详细介绍了CAS原理、悲观锁与乐观锁的区别,展示了在Java中如何利用AtomicInteger实现CAS,讨论了ABA问题及AQS在并发控制中的角色,涵盖了DoubleAdder和LongAccumulator的高效并发计数。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、概述 

1、什么是CAS?

        CAS是Compare-and-swap的缩写。CAS本质上是硬件(CPU)提供的原子比较和交换操作。大多数多处理器架构在硬件中支持 CAS,比较和交换操作是实现基于锁和非阻塞并发数据结构的最流行的同步原语。

        在CAS之前,test-and-set指令是用于将 1 写入(设置)到内存位置并将其旧值作为单个原子(即不可中断)操作返回的指令。然后调用者可以“测试”结果以查看调用是否改变了状态,不过目前在大多现代CPU中test-and-set已经被淘汰。

        现代CPU内置了对原子比较和交换操作的支持。在某些情况下,可以使用比较和交换操作来替代同步块或其他阻塞数据结构。CPU会保证一次只有一个线程可以执行比较和交换操作——即使跨CPU内核也是如此。

2、悲观锁和乐观锁

(1)悲观锁

        悲观锁会因线程一直阻塞导致系统上下文切换,系统的性能开销大。

        当两个线程试图同时进入Java中的一个同步块时,其中一个线程将被阻塞,而另一个线程将被允许进入同步块。当进入同步块的线程再次退出该块时,将允许等待的线程进入该块。

       当同步块再次空闲时,无法准确确定何时解除阻塞的线程。这通常取决于操作系统或执行平台来协调阻塞线程的解除阻塞。当然,这个时间不会很长,但是可能会浪费一些时间用于阻塞线程,因为它本来可以访问共享数据了。

        如下图所示:

(2)乐观锁

        CAS 被认为是一种乐观锁。

        当使用硬件/CPU 提供的比较和交换功能而不是操作系统或执行平台提供的同步、锁定、互斥锁等时,操作系统或执行平台不需要处理线程的阻塞和解除阻塞。这导致线程等待执行比较和交换操作的时间更短,从而导致更少的拥塞和更高的吞吐量。

        如下图所示:

        当然,如果线程在重复执行比较和交换的过程中等待很长时间,可能会浪费大量的CPU周期,而这些CPU周期本来可以用在其他任务(其他线程)上。但在许多情况下,情况并非如此。这取决于共享数据结构被另一个线程使用多长时间。实际上,共享数据结构的使用时间不会很长,因此上述情况不应该经常发生。但同样 - 这取决于具体情况、代码、数据结构、尝试访问数据结构的线程数、系统负载等。 

二、Java中的使用

1、从AtomicInteger出发

        我们从AtomicInteger的源码来理解CAS。

public final int getAndSet(int newValue) {
    return unsafe.getAndSetInt(this, valueOffset, newValue);
}

        进入Unsafe类,这里的while就是所谓的自旋,就是不停顿,一遍一遍的试,直到成功为止。

public final int getAndSetInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var4));

        return var5;
    }

        然后调用native方法

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

         然后进入JVM虚拟机,hotspot/src/share/vm/prims/unsafe.cpp

{CC"compareAndSwapInt",  CC"("OBJ"J""I""I"")Z",      FN_PTR(Unsafe_CompareAndSwapInt)},

        最后在hotspot源码实现中都会调用统一的cmpxchg的指令,类似如下代码,代码来自jdk12-06222165c35f源码中的src\hotspot\os_cpu\linux_x86\atomic_linux_x86.hpp文件。

template<>
template<typename T>
inline T Atomic::PlatformCmpxchg<1>::operator()(T exchange_value,
                                                T volatile* dest,
                                                T compare_value,
                                                atomic_memory_order /* order */) const {
  STATIC_ASSERT(1 == sizeof(T));
  __asm__ volatile ("lock cmpxchgb %1,(%3)"
                    : "=a" (exchange_value)
                    : "q" (exchange_value), "a" (compare_value), "r" (dest)
                    : "cc", "memory");
  return exchange_value;
}

2、ABA问题 

        A-->B--->A 问题,假设有一个变量 A ,修改为B,然后又修改为了 A,实际已经修改过了,但 CAS 可能无法感知,造成了不合理的值修改操作。

        JDK 中 java.util.concurrent.atomic 并发包下,提供了 AtomicStampedReference,通过为引用建立个 Stamp 类似版本号的方式,确保 CAS 操作的正确性。

3、AbstractQueuedSynchronizer 

        AQS作为一个抽象类,是构建JUC包中的锁(比如ReentrantLock)或者其他同步组件(比如CountDownLatch)的底层基础框架。其中均是使用CAS进行原子操作的保证。

        在每一个同步组件的实现类中,都具有AQS的实现类作为内部类,被用来实现该锁的内存语义,并且他们之间是强关联关系,从对象的关系上来说锁或同步器与AQS是:“聚合关系”。比如ReentrantLock。

         AQS提供了一个自定义同步组件的框架,如果需要自定义同步组件,使用AQS是一个好的选择,虽然大多数开发人员可能永远不会直接使用 AQS,因为Java提供的同步组件(锁、原子变量、阻塞队列等等)已经足够满足日常场景了。

4、DoubleAdder、LongAdder、DoubleAccumulator、LongAccumulator

        上面四个类都是从1.8开始存在的,均继承自Striped64类,Striped64类也是基于CAS进行原子操作。

        DoubleAdder、LongAdder 工具类采用了 “分头计算最后汇总” 的思路,避免每一次(细粒度)操作的并发控制,提高了并发的性能。

        参考下面的代码

// 首先创建一个 DoubleAdder 对象
DoubleAdder doubleAdder = new DoubleAdder();
...
// 调用累加方法
doubleAdder.add(50.5);
doubleAdder.add(49.5);
// 调用求和方法
double sum = doubleAdder.sum();

        相比 LongAdder,LongAccumulator 工具类提供了更灵活更强大的功能。不但可以指定计算结果的初始值,相比 LongAdder 只能对数值进行加减运算,LongAccumulator 还能自定义计算规则,比如做乘法运行,或其他任何你想要的计算规则。

        参考代码

//创建计算规则。
LongBinaryOperator selfOperator = new LongBinaryOperator() {
        @Override
        public long applyAsLong(long left, long right) {
            ...
        }
    }

// 接着使用构造方法创建一个 LongAccumulator 对象,这个对象的第1个参数就是一个双目运算器对象,第二个参数是累加器的初始值。
LongAccumulator longAccumulator = new LongAccumulator(selfOperator , 0);
...
// 调用累加方法
longAccumulator.accumulate(1000)
// 调用结果获取方法
long result = longAccumulator.get();

5、JDK源码下载地址

OpenJDK Mercurial Repositorieshttp://hg.openjdk.java.net/jdk

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

坐望云起

如果觉得有用,请不吝打赏

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值