【Java多线程】 CAS —— 一文了解CAS到底是什么

学过多线程就会接触到并发,并发再多线程中的重要性不言而喻,在Java中还有并发包,里面实现了各种各样的方法来帮助我们解决多线程带来的各种问题。而要想读懂这些底层问题,CAS是绕不过的知识,大多底层都是以CAS来实现的。今天就带大家来学习CAS相关的知识。

一、什么是CAS?

CAS: 全称Compare and swap,字面意思:" 比较并交换 "
一个CAS涉及到的操作数有:

  • 内存值V
  • 旧的预期值A
  • 要修改的新值B

有了这三个操作数,在看看它的操作:

  1. 首先输入旧的预期值A 和修改后的新值 B
  2. 对比变量是旧值和内存值是否相同
  3. 如果相同,将旧值改为新值

这也就侧面的表现出了对于多个线程都对某个值进行修改时,保证了修改前拿到的值是期望值才会操作。

二、为什么要有CAS

还记得之前多线程的经典例子嘛,就是多个线程同时对一个共享变量进行修改值,最终修改后的值大概率不是正确的结果。也就是对于i ++ 操作,在多线程中是保证不了它的正确性。
原因呢,就是i ++ 本身并不是一个原子性的操作,它可以分成三步:

  1. 从主内存中读取到 i 的值
  2. 对 i 进行+1 操作
  3. 写回到主内存

这就导致多线程在执行该操作时,线程A、B可能同时从主内存中获得一个值后分别 +1 后写回到主内存。导致结果的错误,这时候你就会想到 那用 呗! 所以解决的方式就是使用synchronized关键字来进行加锁。
的确,这种多线程导致的原子性问题可以加锁,使得众多线程竞争锁,拿到锁的线程才可以进行下一步的操作,其它线程则都开始阻塞,直到这个线程释放了锁后,唤醒其它线程,再次开始竞争锁。
对于线程的阻塞和唤醒都是非常消耗时间的! 如果像i ++ 的操作,仅仅只是每次进行加一操作就要经历线程唤醒和重新竞争锁,未免有些大材小用。

而这时候,就可以使用CAS机制中的compareAndSet方法,也就是比较并设值。
当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号。可见 CAS 其实是一个乐观锁

三、CAS是怎么实现的?

看了上面的解释,你可能还会有些疑惑,CAS怎么保证的在设置值的时候的原子性呢?看起来也是进行了比较值和设置值的操作呀?
其实,这都是操作系统的功劳!在操作系统中这么多操作实际上就是一条指令操作。

针对不同的操作系统,JVM 用到了不同的 CAS 实现原理:

  • java 的 CAS 利用的的是 unsafe 这个类提供的 CAS 操作;
  • unsafe 的 CAS 依赖了的是 jvm 针对不同的操作系统实现的 Atomic::cmpxchg;
  • Atomic::cmpxchg 的实现使用了汇编的 CAS 操作,并使用 cpu 硬件提供的 lock 机制保证其原子性。

看起来很复杂,其实只要知道:是因为硬件予以了支持,软件层面才能做到。

四、CAS有哪些应用?

实现原子类
Java中的atomic包下的原子类,都是通过CAS实现的。

/**
     * Atomically increments by one the current value.
     *
     * @return the updated value
     */
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

可以参考另一篇文章——> Java中atomic包下的原子操作类

五、CAS存在的问题

  1. 典型的就是ABA的问题,也就是先将值改变,再改回去,看起来好像就是没有改变一样,CAS也发现不了这个过程。 对于基本类型的值来说,这种把数字改变了在改回原来的值是没有太大影响的,但如果是对于引用类型的话,就会产生很大的影响了
    📌解决这个问题就是加入版本信息,也就是每一个线程修改值后,版本信息都会改变,这样即使两个线程是持有相同引用,但版本信息却不一致,我们也认为是不一样的。这样也可以防止ABA问题。在Java中的 AtomicStampedReference 这个类是可以提供版本控制的。
  2. 对于线程过多的问题,当太多线程进行CAS,每次判断都会发现值已经不再是期望的原始值,这就会导致很多线程是在白白的空转,效率降低。
    📌为了解决这个问题Java8 引入了一个 cell[] 数组,线程少时,就使用CAS机制。当过多的线程请求时,就会将多个线程分组,并将cell中的元素分配给某一组线程,而这组线程对数的操作就会在cell中进行,到最后,将cell中的元素在进行合并。

由以上着问题我们也可以看出来,CAS由于其不会阻塞线程的特点,而是一直在循环,这就使得它的适用场景就是代码能很快的执行,如果代码执行时间过长,就会导致其它线程调用方法长时间处于失败状态。

好啦,这就是CAS的一些基本的知识了,自己总结的也还有些不到位,如果大家感兴趣,建议研究源码,可以学到很多。如果文章有什么问题,欢迎留言指正。也欢迎点赞关注一起进步😀

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章