无锁编程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)
上一篇 2022年6月10日 下午1:16
下一篇 2022年6月10日 下午1:36


相关推荐

  • 快速提升网站排名_使用快排优化的方法「建议收藏」

      确定自己网站适不适合做快排  好多小伙伴想为自己网站做一做快排,但是又不知道自己网站适不适合,首先,大家都知道选好关键词的重要性,想实现投入和产出的最大化,建设网站前选好合适的关键词是非常必要的。找到网站合适的关键词,根据难度将关键词进行分类,布局对应的页面,核心关键词布局到首页,常用关键词分布列表页或栏目页,长尾关键词则主要出现在内容页。良好的网站结构是这整个网站构建的核心体系。一个良好的网站框架能让排名效果事半功倍,想要被搜索引擎快速收录的前提是可以让蜘蛛排名与抓取,而蜘蛛喜欢得是目录层次简单

    2022年4月15日
    53
  • centos7设置go代理

    centos7设置go代理wgethttps://dl.google.com/go/go1.10.3.linux-amd64.tar.gztar-C/usr/local-zxvfgo1.10.3.linux-amd64.tar.gzvim/etc/profileexportGOROOT=/usr/local/goexportPATH=$PATH:$GOROOT/binexportGOPROXY=https://goproxy.cnsource/etc/profilegoversion..

    2022年7月26日
    15
  • 构建vscode的vue组件代码补全插件以及上传

    构建vscode的vue组件代码补全插件以及上传

    2022年4月2日
    337
  • 【linux 】linux 命令:查看 Linux 服务器配置

    目录一、服务器型号二、操作系统三、CPU四、内存五、硬盘六、其他一、服务器型号dmidecode|grep”SystemInformation”-A9|egrep”Manufacturer|Product”二、操作系统(1)当前操作系统发行版信息:cat/etc/redhat-release(2)操作系统发行版详细信息:lsb_release-a三、CPU(1)CPU统计信息:lscpu(2)CPU型号

    2022年4月14日
    328
  • html样式表优点,css样式表的使用有哪些优点?

    html样式表优点,css样式表的使用有哪些优点?CSS全称CascadingStyleSheet,表示层叠样式表,是一种用来表现HTML(标准通用标记语言的一个应用)或XML(标准通用标记语言的一个子集)等文件样式的计算机语言。CSS不仅可以静态地修饰网页,还可以配合各种脚本语言动态地对网页各元素进行格式化CSS用于改进HTML标记内容的呈现。使用CSS我们可以基于媒体定义不同的内容显示方式。CSS能够对网页中元素位置的排版进行像素级精确…

    2022年7月25日
    10
  • VMWare虚拟机上网的方法

    VMWare虚拟机上网的方法VMWare虚拟机上网的方法1推荐局域网方式:如果主机是在局域网内通过网关或代理上网,那虚拟机的网络方式设为Bridged连接,把IP地址设为同主机在一个网段,比如主机IP是192.168.0.45,网关是192.168.0.1,那虚拟机的IP设为192.168.0.2-254中的一个,注意不要和已有的IP重复,然后网关也设为192.168.0.1,就可以上网了。宽带拨号方式

    2022年5月19日
    46

发表回复

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

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