无锁编程CAS[通俗易懂]

无锁编程CAS[通俗易懂]前言CAS(CompareAndSwap,比较并交换),要说CAS是无锁编程,多多少少有些“标题党”的感觉。因为CAS根据其设计思想,可以划分为乐观锁。不同于synchronized关键字,synchronized实现的是悲观锁。我第一次听说乐观锁和悲观锁的时候有点震惊:一把锁我还得知道它乐不乐观?乐不乐观?一把锁难道还有情绪?实际上乐观锁和悲观锁是基于线程并发竞争的角度来说的,悲观锁就是…

大家好,又见面了,我是你们的朋友全栈君。

前言

CAS(Compare And Swap,比较并交换),要说CAS是无锁编程,多多少少有些“标题党”的感觉。因为CAS根据其设计思想,可以划分为乐观锁。不同于synchronized关键字,synchronized实现的是悲观锁。我第一次听说乐观锁和悲观锁的时候有点震惊:一把锁我还得知道它乐不乐观?乐不乐观?一把锁难道还有情绪?
实际上乐观锁和悲观锁是基于线程并发竞争的角度来说的,悲观锁就是假设每次操作都悲观的认为会发生线程竞争,不加锁就会导致程序结果错误;乐观锁就假设每次操作都乐观的认为不会发生线程竞争,所以不需要上锁,因此CAS被称为无锁编程,实际上是一种乐观锁的体现。

Atomic

先看两个常见的例子

public class AtomicDemo {

    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 100000; i++) {
                count++;
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 100000; i++) {
                count++;
            }
        });

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        System.out.println(count);
    }
}

两个线程累加同一个共享变量,线程不安全,输出的count小于等于200000。相信大家已经非常清楚为什么线程不安全了,所以想要解决这个问题也很简单,加锁就行。
但是这里如果要用无锁编程CAS来解决的话该怎么解决呢?
只需要简单的修改下代码

public class AtomicDemo {

    // AtomicInteger变量
    private static AtomicInteger count = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 100000; i++) {
                count.getAndIncrement();  // 等价于线程安全的count++操作
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 100000; i++) {
                count.getAndIncrement();   // 等价于线程安全的count++操作
            }
        });

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        System.out.println(count.get());
    }
}

输出结果

200000

无论运行多少遍,结果都是200000。也就是说在没有加锁的情况下,写出了线程安全的count++操作。相比大家已经看到了,实现的关键就是AtomicInteger#getAndIncrement()方法。所以我们直接看下getAndIncrement()的源码。
代码位置:java.util.concurrent.atomic.AtomicInteger

/**
 * Atomically increments by one the current value.
 * 以原子方式将当前值增加一
 */
public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

由于该方法调用了unsafe.getAndAddInt(...)方法,继续往下追踪
代码位置:sun.misc.Unsafe

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;
}

可以该实现主要调用了this.compareAndSwapInt(...)方法,该方法就是便是CAS(Compare And Swap,比较并交换)。既然是比较和交换,那我们应该明确两点:比较什么、交换什么?
CAS操作涉及到三个变量(V、E、N),V表示要更新的变量(工作内存中该变量的值)、E表示期望值(主内存中该变量的值)、N表示新值。
CAS实现原理
首先判断变量当前值(V)是否等于期望值(E),不等于则说明在当前线程修改这个变量,同步回主内存之前,有别的线程已经修改过这个变量并且同步回了主内存。所以当前线程不能把值同步回主内存,而是重新从主内存中读取该值,重复这整个操作,直到当前值(V)等于期望值(E),才将新值同步回主内存。再次看看CAS实现的源码

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
    	// var5就是期望值
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

// JVM保证该方法获取的就是变量的最新值
public native int getIntVolatile(Object var1, long var2);

// JVM保证该方法的CAS操作是原子操作
// var1是变量所处类的实例
// var2是变量的偏移量,和var1一起可以获取变量的值
// var4是期望值
// var5是新值
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

整个过程描述得更通俗一点就是:
1、线程A从主内存中读入变量count作为值V;
2、线程A读取count的最新值,作为期望值E
3、线程A把值(V)和期望(E)比较是否相等,相等就把新值(N)写回主内存,不相等就回到操作1
第三步是原子操作,比较V和E就是为了保证变量count没有被其他线程修改过。

以上就是CAS无锁编程的实现原理。

CAS缺陷

CAS并不是像降龙十八掌那样横扫一切的存在,它也有自己的缺陷。具体体现在以下三点:

  • 自旋的实现方式让所有线程都处于高频运行,争抢CPU的状态,如果操作长时间不成功,会带来很大的CPU消耗
  • 仅针对单个变量的操作,不能用于多个变量来实现原子操作
  • 会存在ABA问题

ABA问题是指主内存中该变量的值从A变成B,再变成A的这种情况。
回头看上文描述的三个步骤。假设第一步线程A从主内存中读入的count=100,此时线程B把变量count改成了101,线程C又把变量count的值改成了100。此时线程A执行第二个步骤,读取count最新值count为100,作为期望值。虽然数值上没什么问题,但是此100已经非彼100了,这就是ABA问题,对线程A来说,无法感知数据的变化。如果业务上完全不在意ABA的影响,才可以用CAS。

总结

CAS和Synchronized各有优势,只是适用场景不同。明确两者的区别和适用场景,才能写出更优雅的并发编程代码。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/132695.html原文链接:https://javaforall.net

(0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • sqlserver 属性 TextHeader 不可用于 StoredProcedure“[dbo].[x]”该对象可能没有此属性,也可能是访问权限不足而无法检索。 该文本已加密。

    sqlserver 属性 TextHeader 不可用于 StoredProcedure“[dbo].[x]”该对象可能没有此属性,也可能是访问权限不足而无法检索。 该文本已加密。打开sqlserverproc存储过程错误:属性TextHeader不可用于StoredProcedure“[dbo].[x]”。该对象可能没有此属性,也可能是访问权限不足而无法检索。 该文本已加密。(Microsoft.SqlServer.Smo),提示如下图错误:注:本文基于SQLserver2008R2,其他版本没有测试过解决方法:1、使用原有数据库从新导出非加密脚本重新建立…

    2022年7月26日
    21
  • python编程考试有哪些(python编程考试模拟题)

    python编程考试有哪些(python编程考试模拟题)2021国内外主流机器人编程赛事+等级考试Scratch编程、C++编程、Python编程等多个赛项,评比类、竞技类不同比赛形式自主选择。多个国内外主流机器人编程赛事,总能帮助孩子找到施展能力、表现创意的舞台。机器人、编程、人工智能等级考试篇全国青少年机器人技术等级考试和全国青少年软件编程等级考试均由中国电子…。2021机器人编程赛事+等级考试攻略之国内外主流赛事及能力测评篇上周,玛酷在公众号发布了一篇名为《2021机器人编程赛事+等级考试攻略之教育部白名单赛事篇》的文章。文章中为大家介绍了20

    2022年5月17日
    62
  • open函数返回值为0

    open函数返回值为0open函数是我们开发中经常会遇到的,这个函数是对文件设备的打开操作,这个函数会返回一个句柄fd,我们通过这个句柄fd对设备文件读写操作。  我们在对这个fd作判断的时候,经常会用到:    fd=open(filename,O_RDONLY);     If(fd          Printf(“open%serror!\n”,fi

    2022年5月25日
    305
  • pycharm配置flask环境_调试是什么意思

    pycharm配置flask环境_调试是什么意思1.Flask的调试模式​ 通过调用run()方法启动Flask应用程序。但是,当应用程序正在开发中时,应该为代码中的每个更改手动重新启动它。为避免这种不便,请启用调试支持。如果代码更改,服务器将自行重新加载。它还将提供一个有用的调试器来跟踪应用程序中的错误(如果有的话)。在运行或将调试参数传递给run()方法之前,通过将application对象的debug属性设置为True来启用Debug模式。app.debug=Trueapp.run(debug=True)但是在pycharm编译器

    2022年8月29日
    0
  • java编译和运行

    java编译和运行java应用程序的基本结构 编写源文件 保存源文件 额外附加 编译器(javac.exe) 解释器(java.exe)总结:假如我的B.java源文件在C:\Users\AUSU\Desktop\ts里面一般都是进入到这个目录里面编译解释编译:javacB.java解释:javaB注意:解释不可能以带目录的方式去运行程序,编译可以零…

    2022年6月14日
    27
  • ETH显卡矿机_eth矿机组装

    ETH显卡矿机_eth矿机组装显卡矿机搭建选择合适显卡选择硬件选择挖矿软件挖矿系统mineros挖矿软件注意:每个币种的软件都不一样挖矿系统和软件也有多种具体对应的官网都会有教程选择合适显卡主流显卡算力对比选择硬件选择挖矿软件前提准备自己的钱包地址选择矿池地址挖矿系统mineros步骤:注册账号刻盘启动挖矿和监控矿机状态挖矿软件NBMiner…

    2022年9月27日
    0

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号