详解CAS算法

这篇具有很好参考价值的文章主要介绍了详解CAS算法。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

CAS的全称是 Compare And Swap(比较并交换),它是并发编程中的一个重要概念。本文结合Java的多线程操作来讲解CAS算法。

CAS算法的优势是可以在不加锁的情况下保证线程安全,从而减少线程之间的竞争和开销。

目录

一、CAS算法的内容

1、基本思想和步骤

2、CAS伪代码(如果把CAS想象成一个函数)

二、CAS算法的应用

1、实现原子类

*伪代码实现原子类

2、实现自旋锁

*伪代码实现自旋锁

三、CAS的ABA问题

1、ABA问题引发的BUG

2、解决ABA问题——使用版本号


一、CAS算法的内容

1、基本思想和步骤

CAS算法的基本思想是,先比较内存M中的值与寄存器A中的值(旧的预期值,expectValue)是否相等,如果相等,则将寄存器B中的值(新值,swapValue)写入内存;如果不相等,则不做任何操作。整个过程是原子的,不会被其他并发操作中断。

详解CAS算法,# JavaWeb,算法,jvm,并发编程,Java,多线程

这里虽然涉及到内存与寄存器值的“交换”,但更多时候我们并不关心寄存器中存的值是什么,而是更关心内存中的数值(内存中存的值即变量的值)。所以这里的“交换”可以不用看成交换,直接看成是赋值操作就行,即把寄存器B中的值直接赋值到内存M中了。

CAS 操作包括三个操作数:一个内存位置(通常是共享变量)、期望的值和新值。CAS 操作的执行过程如下:

  1. 读取内存位置的当前值。
  2. 比较当前值与期望的值是否相等。
  3. 如果相等,将新值写入内存位置;否则,操作失败。

2、CAS伪代码(如果把CAS想象成一个函数)

boolean CAS(address, expectValue, swapValue) {
 if (&address == expectedValue) {
   &address = swapValue;
        return true;
   }
    return false;
}

详解CAS算法,# JavaWeb,算法,jvm,并发编程,Java,多线程

上面给出的代码只是伪代码,并不是真实的CAS代码。事实上,CAS操作是一条由CPU硬件支持的、原子的硬件指令,这一条指令就能完成上述这一段代码的功能。

“一条指令”和“一段代码”的最大区别在于原子性。上述这一段伪代码不是原子的,运行过程中可能随着线程的调度有概率产生线程安全问题;但原子的指令不会有线程安全问题。

同时,CAS也不会有内存可见性的问题,内存可见性相当于是编译器把一系列指令的进行了调整,把读内存的指令调整成读寄存器的指令。但CAS本身就是指令级别读取内存的操作,所以不会有内存可见性带来的线程不安全问题。

因此,CAS可以做到不加锁也能一定程度地保证线程安全。这样就引申出基于CAS算法的一系列操作。


二、CAS算法的应用

CAS可以用于实现无锁编程。实现原子类和实现自旋锁就是无锁编程的其中两个方式。

1、实现原子类

标准库 java.util.concurrent.atomic 包中有很多类使用了很高效的机器级指令而不是使用锁) 来保证其他操作的原子性

例如Atomiclnteger 类提供了方法 incrementAndGet、getAndIncrement 和 decrementAndGet、getAndDecrement,它们分别以原子方式将一个整数自增或自减。

        AtomicInteger num = new AtomicInteger(0);
        Thread t1 = new Thread(()->{
                //num++
                num.getAndIncrement();
                //++num
                num.incrementAndGet();
                //num--
                num.getAndDecrement();
                //--num
                num.decrementAndGet();
        });

例如,可以安全地生成数值序列,如下所示

import java.util.concurrent.atomic.AtomicInteger;

public class Test2 {
    public static void main(String[] args) throws InterruptedException {
        AtomicInteger num = new AtomicInteger(0);
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                //num++
                num.getAndIncrement();
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                //num++
                num.getAndIncrement();
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();
        System.out.println(num.get());
    }
}

运行结果:最终num的值正好是100000

详解CAS算法,# JavaWeb,算法,jvm,并发编程,Java,多线程

这是因为 getAndIncrement() 方法以原子的方式获得 num 的值,并将 num 自增也就是说, 获得值、 1 并设置然后生成新值的操作不会中断可以保证即使是多个线程并发地访问同一个实例,也会计算并返回正确的值

通过查看源码可以发现,getAndIncrement() 方法并没有用到加锁(synchronized):

详解CAS算法,# JavaWeb,算法,jvm,并发编程,Java,多线程

但再进入 getAndAddInt 方法可以发现,其中用到了 CAS 算法:

详解CAS算法,# JavaWeb,算法,jvm,并发编程,Java,多线程

再进入 compareAndSwapInt 方法后会发现,这是一个由 native 修饰的方法。CAS算法的实现依赖于底层硬件和操作系统提供的原子操作支持,因此它是更偏向底层的操作。 

补充 - 与之形成对比的线程不安全的案例是:

下面就是一个线程不安全的例子。该代码中创建了一个counter变量,同时分别创建了两个线程t1和t2,让这两个线程针对同一个counter令其自增5w次。

class Counter {
    private int count = 0;
 
    //让count增加
    public void add() {
        count++;
    }
 
    //获得count
    public int get() {
        return count;
    }
}
public class Test {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
 
        // 创建两个线t1和t2,让这两个线程分别对同一个counter自增5w次
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
 
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
 
        t1.start();
        t2.start();
 
        // main线程等待两个线程都执行结束,然后再查看结果
        t1.join();
        t2.join();
 
        System.out.println(counter.get());
    }
}

按理来说,最终输出counter的结果应当是10w次。但我们运行程序后发现,不但结果不是10w,而且每次运行的结果都不一样——实际结果看起来像一个随机值。

详解CAS算法,# JavaWeb,算法,jvm,并发编程,Java,多线程

由于线程的随即调度,count++这一语句并不是原子的,它本质上是由3个CPU指令构成:

  1. load。把内存中的数据读取到CPU寄存器中。
  2. add。把寄存器中的值进行 +1 运算。
  3. save。把寄存器中的值写回到内存中。

CPU需要分三步走才能完成这一自增操作。如果是单线程中,这三步没有任何问题;但在多线程编程中,情况就会不同。由于多线程调度顺序是不确定的,实际执行过程中,这俩线程的count++操作的指令排列顺序会有很多种不同的可能:

详解CAS算法,# JavaWeb,算法,jvm,并发编程,Java,多线程

上面只列出了非常小的一部分可能,实际中有更多可能的情况。而不同的排列顺序下,程序执行的结果可能是截然不同的!比如以下两种情况的执行过程:

详解CAS算法,# JavaWeb,算法,jvm,并发编程,Java,多线程

因此, 由于实际中线程的调度顺序是无序的,我们并不能确定这俩线程在自增过程中经历了什么,也不能确定到底有多少次指令是“顺序执行”的,有多少次指令是“交错执行”的。最终得到的结果也就成了变化的数值,count一定是小于等于10w的

(来自文章:Java多线程基础-6:线程安全问题及解决措施) 

*伪代码实现原子类

代码:

class AtomicInteger {
    private int value;
    public int getAndIncrement() {
        int oldValue = value;
        while ( CAS(value, oldValue, oldValue+1) != true) {
            oldValue = value;
       }
        return oldValue;
   }
}

详解CAS算法,# JavaWeb,算法,jvm,并发编程,Java,多线程

上面代码中,虽然看似刚刚把 value 赋值给 oldValue 后,就紧接着比较 value 和 oldvalue是否相等,但比较结果依然是可能不相等的。因为这是在多线程的环境下。value 是成员变量,如果两个线程同时调用 getAndIncrement 方法,就有可能出现不相等的情况。其实此处的 CAS 就是在确认当前 value 是否变过。如果没变过,才能自增;如果变过了,就先更新值,再自增。

之前的线程不安全,有一个很大的原因就是一个线程不能及时感知到另一个线程对内存的修改:

详解CAS算法,# JavaWeb,算法,jvm,并发编程,Java,多线程

之前线程不安全,是因为t1在自增的时候先读后自增。此时在t1自增之前,t2已经自增过了,t1是却还是在一开始的0的基础上自增的,此时就会出现问题。

但CAS操作使得t1在执行自增之前,先比较一下寄存器和内存中的值是否一致,只有一致了才执行自增,否则就重新将内存中的值向寄存器同步一下。

这个操作不涉及阻塞等待,因此会比之前加锁的方案快很多。

2、实现自旋锁

自旋锁是一种忙等待锁的机制。当一个线程需要获取自旋锁时,它会反复地检查锁是否可用,而不是立即被阻塞。如果获取锁失败(锁已经被其他线程占用),当前线程会立即再尝试获取锁,不断自旋(空转)等待锁的释放,直到获取到锁为止。第一次获取锁失败,第二次的尝试会在极短的时间内到来。这样能保证一旦锁被其他线程释放,当前线程能第一时间获取到锁。一般乐观锁的情况下(锁冲突概率低),实现自旋锁是比较合适的。

*伪代码实现自旋锁

public class SpinLock {
    private Thread owner = null;
    public void lock(){
        // 通过 CAS 看当前锁是否被某个线程持有
        // 如果这个锁已经被别的线程持有, 那么就自旋等待
        // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程
        while(!CAS(this.owner, null, Thread.currentThread())){
        
        }
   }
    public void unlock (){
        this.owner = null;
   }
}

详解CAS算法,# JavaWeb,算法,jvm,并发编程,Java,多线程


三、CAS的ABA问题

CAS的ABA问题是使用CAS时遇到的一个经典的问题。

已知 CAS 的关键是对比内存和寄存器中的值,看二者是否相同,就是通过这个比较来判断内存中的值是否发生过改变。然而,万一对比的时候是相同的,但其实内存中的值并不是没变过,而是从A值变成B值后又变回了A值呢?

此时,有一定概率会出问题。这样的情况就叫做ABA问题。CAS只能对比值是否相同,但不能确定这个值是否中间发生过改变。

详解CAS算法,# JavaWeb,算法,jvm,并发编程,Java,多线程

这就好比我们从某鱼上买了一个手机,但无法判定这个手机是刚出厂的新手机,还是别人用旧过了之后又翻新过的翻新机。

1、ABA问题引发的BUG

其实大部分的情况下ABA问题影响并不大。但是不排除一些特殊情况:

假设小明有 100 存款。他想从 ATM 取 50 块钱。取款机创建了两个线程,并发地来执行 -50

(从账户中扣款50块钱)这一操作。

我们期望两个线程中一个线程执行 -50 成功,另一个线程 -50 失败。如果使用 CAS 的方式来完成这个扣款过程,就可能出现问题。

正常的过程

  1. 存款 100。线程1 获取到当前存款值为 100,期望值更新为 50;线程2 获取到当前存款值为 100,望值更新为 50。
  2. 线程1 执行扣款成功,存款被改成 50;线程2 阻塞等待中。
  3. 轮到线程2 执行了,发现当前存款为 50,和之前读到的 100 不相同,执行失败。

异常的过程

  1. 存款 100。线程1 获取到当前存款值为 100,期望值更新为 50;线程2 获取到当前存款值为 100,望值更新为 50。
  2. 线程1 执行扣款成功,存款被改成 50。线程2 阻塞等待中。
  3. 在线程2 执行之前,小明的朋友正好给小明转账了 50,此时小明账户的余额又变成了 100!
  4. 轮到线程2 执行了,发现当前存款为 100, 和之前读到的 100 相同, 再次执行扣款操作。

这个时候, 扣款操作被执行了两次!都是 ABA 问题引起的。

2、解决ABA问题——使用版本号

ABA 问题的关键是内存中共享变量的值会反复横跳。如果约定数据只能单方向变化,问题就迎刃而解了。

由此引入“版本号” 这一概念。约定版本号只能递增(每次修改变量时,都会增加一个版本号)。而且每次 CAS 在对比的时候,对比的就不是数值本身,而是对比版本号。这样其他线程在进行 CAS 操作时可以检查版本号是否发生了变化,从而避免 ABA 问题的发生。

(以版本号为基准,而不以变量数值为基准。约定了版本号只能递增,就不会出现ABA这样的反复横跳问题。)

详解CAS算法,# JavaWeb,算法,jvm,并发编程,Java,多线程

不过在实际情况中,大部分情况下即使遇到了ABA问题,也没有什么关系。知晓版本号可以用来解决ABA问题即可。文章来源地址https://www.toymoban.com/news/detail-560708.html

到了这里,关于详解CAS算法的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Java并发编程第6讲——线程池(万字详解)

    Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池,本篇文章就详细介绍一下。 定义:线程池是一种用于管理和重用线程的技术(池化技术),它主要用于提高多线程应用程序的性能和效率。 ps:线程池、连接池、内存池

    2024年02月11日
    浏览(41)
  • 【并发编程】CAS到底是什么

    Java实现CAS的原理 | Java程序员进阶之路 美团终面:CAS确定完全不需要锁吗? CAS 是 Compare-And-Swap (比较并交换)的缩写,是一种 轻量级的同步机制 ,主要用于实现多线程环境下的无锁算法和数据结构,保证了并发安全性。它可以在 不使用锁 (如synchronized、Lock)的情况下,对

    2024年02月20日
    浏览(42)
  • JUC并发编程16 | CAS自旋锁

    是什么,干什么,解决了什么痛点?如何解决,如何使用。 原子类:java.util.concurrent.atomic 在没有CAS之前,多线程环境不使用原子类保证线程安全i++等操作,会出现数据问题,如果直接加锁synchronized,资源的开销就比较大 在出现CAS之后,多线程环境,使用原子类保证线程安全

    2024年02月04日
    浏览(42)
  • java语法(二)线程并发、Juit单元测试、反射机制、注解、动态代理、XML解析、JVM

    正则表达式验证网站 1、 ? :表示前边这个字符可以出现0次或者1次。例如下边 /used? 既可以匹配 use 也可以匹配 used 。 2、 * :匹配0个或者多个字符, * 号代表前边这个字符可以出现0次或者多次。例如 /ab*c 可以匹配 ac、abc、abbbbc 3、 + :与 * 号不同的是, + 需要前面这个字符

    2024年02月06日
    浏览(53)
  • JUC并发编程学习笔记(十八)深入理解CAS

    什么是CAS 为什么要学CAS:大厂你必须深入研究底层!有所突破! java层面的cas-------compareAndSet compareAndSet(int expectedValue, int newValue) 期望并更新,达到期望值就更新、否则就不更新! Unsafe类 java不能直接操作内存,但是可以调用c++,c++可以操作内存,java可以通过native定义

    2024年02月05日
    浏览(60)
  • Java并发编程面试题——线程池

    参考文章: 《Java 并发编程的艺术》 7000 字 + 24 张图带你彻底弄懂线程池 (1) 线程池 (ThreadPool) 是一种用于 管理和复用线程的机制 ,它是在程序启动时就预先创建一定数量的线程,将这些线程放入一个池中,并对它们进行有效的管理和复用,从而在需要执行任务时,可以从

    2024年02月07日
    浏览(51)
  • 【Java 并发编程】一文读懂线程、协程、守护线程

    在 Java 线程的生命周期一文中提到了 就绪状态的线程在获得 CPU 时间片后变为运行中状态 ,否则就会在 可运行状态 或者 阻塞状态 ,那么系统是如何分配线程时间片以及实现线程的调度的呢?下面我们就来讲讲线程的调度策略。 线程调度是指系统为线程分配 CPU 执行时间片

    2023年04月08日
    浏览(60)
  • Java面试_并发编程_线程基础

    进程是正在运行程序的实例, 进程中包含了线程, 每个线程执行不同的任务 不同的进程使用不同的内存空间, 在当前进程下的所有线程可以共享内存空间 线程更轻量, 线程上下文切换成本一般上要比进程上下文切换低(上下文切换指的是从一个线程切换到另一个线程) 并发是单个

    2024年02月07日
    浏览(54)
  • 【Java并发编程】变量的线程安全分析

    1.成员变量和静态变量是否线程安全? 如果他们没有共享,则线程安全 如果被共享: 只有读操作,则线程安全 有写操作,则这段代码是临界区,需要考虑线程安全 2.局部变量是否线程安全 局部变量是线程安全的 当局部变量引用的对象则未必 如果给i对象没有逃离方法的作用

    2024年02月08日
    浏览(56)
  • 【Java并发编程】线程中断机制(辅以常见案例)

    本文由浅入深介绍了中断机制、中断的常见案例和使用场景。 因为一些原因需要取消原本正在执行的线程。我们举几个栗子: 假设踢足球点球时,A队前4轮中了4个球,B队前4轮只中了2个球,此时胜负已分,第5轮这个点球就不用踢了,此时需要停止A队的线程和B队的线程(共

    2024年02月13日
    浏览(38)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包