【JUC】CAS 底层原理

本贴最后更新于 1022 天前,其中的信息可能已经时移世改

比较并交换(compare and set)

CAS 翻译成中文即:比较并交换,他是一条 CPU 原语操作(保证读写的原子性),底层基于 c/c++ 实现,直接通过指针操作内存实现。通过使用 CAS 原语能够解决并发更新数据的问题,不用额外加锁去保证线程安全。

CAS 的使用

以下例子,通过 AtomicInteger 类的两个方法阐述 CAS 的使用。

getAndIncrement()

有个面试高频考点,i++ 线程安全吗?
实际上在多线程环境下,基于 JMM 模型,每个线程都有各自的本地内存,线程工作时会从主内存拷贝一份自己需要的数据,在值回写主内存时容易引发线程安全问题。

比如两个线程 t1,t2 同时进行 i++ 操作,

  1. t1 与 t2 同时从主内存拷贝了 i=0 的值到本地内存
  2. t1 率先完成了两次 ++ 操作,此时主内存值为 2。t2 因故挂起未执行,且 t2 的本地内存值为 0。
  3. t2 被唤醒,执行 1 次 ++ 操作,回写主内存,将主内存的值覆盖为 1,至此引发线程安全问题。

有同学可能要讲,加上 volatile 保证可见性,就不会有上述问题了,那我们继续看看:
首先 i++ 实际上是由四条指令组成的

第一条指令 getfield :取出 n 这个元素
第二条指令 iconst_1 :把常量 1 压入栈
第三条指令 iadd :把 n 和常量 1 进行相加
第四条指令 putfield :把数据刷回主内存

t1 与 t2 即便能读取到最新的值,但盖不住 i++ 操作非原子,上述地三条指令若同时被执行,则同样引发线程安全问题。

volatile 能够保证安全性,但是不能保证原子性,它仅能保证读到最新的数据。

跑代码看看:

@Data
public class VolatileTestData {
    /**
     * 普通int类型
     */
    private int number = 0;
    /**
     * 原子类int
     */
    private AtomicInteger atomicInteger = new AtomicInteger();

    /**
     * 使用++操作,线程不安全。
     *
     * @return: void
     * @author openshell
     * @date: 2022/2/3 10:48
     */
    public void add() {
        number++;
    }

    /**
     * 基于CPU原语,CAS原理进行自增,线程安全。
     *
     * @return: void
     * @author openshell
     * @date: 2022/2/3 10:48
     */
    public void atomicAdd() {
        atomicInteger.getAndIncrement();
    }
}
@Test
    public void testAtomicity() {
        VolatileTestData volatileTestData = new VolatileTestData();
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    //调用i++方法自增
                    volatileTestData.add();
                    //使用AtomicInteger自增(CAS)
                    volatileTestData.atomicAdd();
                }
            }).start();
        }
        //注:
        //1.Thread.activeCount()用于返回当前线程的线程组中活动线程的数量,返回的值只是一个估计值,因为当此方法遍历内部数据结构时,线程数可能会动态更改。
        //2.此处无内部数据结构故无影响
        //3.IntelliJ IDEA执行用户代码的时候,实际是通过反射方式去调用,而与此同时会创建一个Monitor Ctrl-Break 用于监控目的,故线程会多一个。
        while (Thread.activeCount() > 2) {
            System.out.println("线程数量:" + Thread.activeCount());
            //线程回退到可运行状态,将cpu时间交给同级或者更高优先级的线程
            Thread.yield();
        }
        System.out.println("普通i++的执行结果:" + volatileTestData.getNumber());
        System.out.println("Atomically increments的结果:" + volatileTestData.getAtomicInteger());
    }

执行结果:

线程数量:4
线程数量:4
普通i++的执行结果:18306
Atomically increments的结果:20000

多次运行 i++ 操作均无法满足期望值,原子整型的计算结果总是正确的。

compareAndSet()

运行:

AtomicInteger atomicInteger = new AtomicInteger(5);
        System.out.println(atomicInteger.compareAndSet(5, 2021) + "\t 当前值为:" + atomicInteger.get());
        System.out.println(atomicInteger.compareAndSet(5, 1024) + "\t 当前值为:" + atomicInteger.get());

输出:

true	 当前值为:2021
false	 当前值为:2021

compareAndSet 源码:

若当前值等于期望值,则原子的更新给定的值。

/**
     * Atomically sets the value to the given updated value
     * if the current value {@code ==} the expected value.
     *
     * @param expect the expected value
     * @param update the new value
     * @return {@code true} if successful. False return indicates that
     * the actual value was not equal to the expected value.
     */
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

该方法调用了 unsafe 类,unsafe 位于 jre 下的 rt.jar,由于该类脱离了 jvm 的管理,直接操作内存,可能带来不可预估的错误,故取名为 unsafe。

CAS 的缺点

尽管 cas 算法保证了数据读写的原子性,但仍存在一些问题。

  • ABA 问题
    eg:有三个线程 t1 t2 t3,同时修改变量 i,t1 与 t2 的功能是将 i 改为 B,t3 的功能是将 i 改为 A:

    1. t1 执行成功,i=B,t2 被挂起
    2. t3 被唤醒,将 i 改为 A
    3. t2 被唤醒,此时值为 A,t2 仍然执行 i=B 的操作,t2 并不知道第二步的发生,至此产生 ABA 问题

    解决办法:CAS 属于乐观锁,可以在使用时增加版本号,解决以上问题。

代码示例:

public class ABAResolveTest {

    public static void main(String[] args) {
        testStamp();
    }

    private static void testStamp() {
        AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(1, 1);

        new Thread(()->{
            int[] stampHolder = new int[1];
            //返回当前相同引用的值和信号量
            int value = atomicStampedReference.get(stampHolder);
            int stamp = stampHolder[0];
            System.out.println("thread 1 read value: " + value + ", stamp: " + stamp);

            // 阻塞1s
            LockSupport.parkNanos(1000000000L);

            if (atomicStampedReference.compareAndSet(value, 3, stamp, stamp + 1)) {
                System.out.println("thread 1 update from " + value + " to 3");
            } else {
                System.out.println("thread 1 update fail!");
            }
        }).start();

        new Thread(()->{
            int[] stampHolder = new int[1];
            int value = atomicStampedReference.get(stampHolder);
            int stamp = stampHolder[0];
            System.out.println("thread 2 read value: " + value + ", stamp: " + stamp);
            if (atomicStampedReference.compareAndSet(value, 2, stamp, stamp + 1)) {
                System.out.println("thread 2 update from " + value + " to 2");

                // do sth

                value = atomicStampedReference.get(stampHolder);
                stamp = stampHolder[0];
                System.out.println("thread 2 read value: " + value + ", stamp: " + stamp);
                if (atomicStampedReference.compareAndSet(value, 1, stamp, stamp + 1)) {
                    System.out.println("thread 2 update from " + value + " to 1");
                }
            }
        }).start();
    }
}
  • 循环可能导致 CPU 开销过大
    在 unsafe 源码中可以看出,若未能按照期望值更新,该方法会一直循环下去,这对 CPU 的开销是极大的。
    可以通过增加次数限制解决该问题。
  • 只能保证一个共享变量的原子操作
    如果要保证一个代码块的原子性,CAS 则不支持。此时,可以通过加锁,或者将多个共享变量转化为一个执行 CAS。

参考文章:死磕 java 并发包之 AtomicStampedReference 源码分析(ABA 问题详解) - 掘金 (juejin.cn)

  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3187 引用 • 8213 回帖
  • JUC
    17 引用 • 3 回帖 • 1 关注
1 操作
openshell 在 2022-02-03 18:26:13 更新了该帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...