准备好了吗?带你读底层!深入理解CAS ,从juc原子类一路追溯到unsafe类再到ABA原子引用!

news/2024/5/20 7:24:05 标签: 并发编程, 多线程, juc, java,

一:什么是CAS?

CAS,在Java并发应用中通常指CompareAndSwap或CompareAndSet,即比较并交换。

CAS是一个原子操作 是用于实现多线程同步的原子指令,它比较一个内存位置的值并且只有相等时修改这个内存位置的值为新的值,保证了新的值总是基于最新的信息计算的,如果有其他线程在这期间修改了这个值则CAS失败。CAS返回是否成功或者内存位置原来的值用于判断是否CAS成功。

这里我们用一段短代码来说明:

这里我们用AtomicInteger类来做测试,其底层也是用CAS原理实现的 ,这个类中有一个compareAndSet(int except,int update)—>比较并交换 !,传入的参数一个是期望,一个是达到期望后所更新的值。

java">import java.util.concurrent.atomic.AtomicInteger;

/**
 * @program: juc
 * @description
 * @author: 不会编程的派大星
 * @create: 2021-04-28 20:48
 **/
public class CasTest {


    public static void main(String[] args) {

        AtomicInteger atomicInteger = new AtomicInteger(1000);


        //先比较是否达到期望值,达到就更新,这里达到1000,所以更新值为1200
        System.out.println(atomicInteger.compareAndSet(1000, 1200));

        System.out.println(atomicInteger.get());



        //这里会打印false,因为执行到这里的时候,值已经为1200了,比较发现没达到1000的期望值,所以不更新
        System.out.println(atomicInteger.compareAndSet(1000, 1200));

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

执行结果:
在这里插入图片描述

注:可以看到,当第二次调用compareAndSet方法的时候,这里打印的是false,即交换失败,因为执行到这里的时候,值已经为1200了,比较发现没达到1000的期望值,所以不更新,即值还是1200

CAS就可以这样理解:如果我期望的值达到了,那么就更新,否则, 就不更新, CAS 是CPU的并发原语!

透过其实现细节来看,我们可以看到原子类是由unsafe类的compareAndSwapInt方法实现的?
在这里插入图片描述

那么unsafe类究竟是什么呢?

二、unsafe类以及compareAndSwap方法

**unsafe:**我们都知道java是无法直接操作内存的,但是java可以调用c++去操作内存,也就是我们常说的native方法,这里的unsafe类可以说是java留的后门,可以通过这个类操作内存!
在这里插入图片描述
注:valueOffset:内存地址偏移值 ,这里value加了volatile,保证了其可见性,和禁止指令重排!

我们通过atomicInteger中的getAndAddInt方法来一探究竟吧
在这里插入图片描述
注:这里就让我们来详细说明一下各个参数吧
var1也就是当前的对象,var2也就是当前内存的偏移量,delta就是需要加的数值,再往下看,var5也就是当前对象var1偏移var2后的值,也就是说var5是获取当前对象内存中的值,再往下看,最重要的compareAndSwapInt来了,这里我们分隔一下这几个参数:
在这里插入图片描述
第一个参数1就是获取当前对象var1内存偏移var2的值,然后比较参数1和参数2,比较是否相等,也就是上面提到的能不能达到我们的except,如果期望达到了,就更新,更新为参数3,也就是在原来的值的基础上在加上一个新的数var4,所以归更到底还是 先比较再更新的思想 。还有一点,这里的do–while相当于一个自旋,一直旋转,直到成功为止。
这里是直接操作内存,所以效率极高!可比lock和synchronized高多了!

*嗯~~~~~妙不可言!*

讲到这里,我们来总结下看看CAS到底是什么吧?

CAS:比较当前工作内存中的值和主内存中的值,如果这个值是期望的,那么则执行操作!如果不是就

一直循环!

但还是存在缺点:
1.因为自旋的存在,循环胡耗时!
2.一次性只能保证一个共享变量的原子性
3.ABA问题

你以为面试官问你到这儿就不问了,那你可错了,都到unsafe了,不再问你两句ABA问题是怎么解决的?

三、ABA问题
ABA 问题(狸猫换太子)

图解:

在这里插入图片描述

注:这边B线程先通过CAS将1改为3,再将3改为1,此时A线程在去改,虽然能改成功,但是A这边是不知道B这边已经修改过数据了,而我们期望的效果是A在修改之前是已经知道B修改过值了

简单代码实现:

java">import sun.plugin2.message.GetAppletMessage;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * @program: juc
 * @description
 * @author: 不会编程的派大星
 * @create: 2021-04-29 19:37
 **/
public class AbaTest {

    public static void main(String[] args) {

        //初始值为10
        AtomicInteger atomicInteger = new AtomicInteger(10);

        System.out.println(atomicInteger.get());


        //模拟捣乱线程 先将10 改为15,在将15改回10
        System.out.println(atomicInteger.compareAndSet(10, 15));

        System.out.println(atomicInteger.get());

        System.out.println(atomicInteger.compareAndSet(15, 10));

        System.out.println(atomicInteger.get());


        //正常线程  将10改为15
        System.out.println(atomicInteger.compareAndSet(10, 15));

        System.out.println(atomicInteger.get());


    }
}

注:这里即使正常线程CAS成功了,但实际开发中这种问题可能会造成很严重的问题!

那应该怎么解决呢?

四、原子引用解决ABA问题

原子引用对应的思想其实就是乐观
接着上面的问题继续

简单代码实现:

java">import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;

/**
 * @program: juc
 * @description
 * @author: 不会编程的派大星
 * @create: 2021-04-29 19:42
 **/
public class LgAbaTest {


    public static void main(String[] args) {

        //这里的initialStamp可以理解为版本号,每成功修改一次,版本号变一次,我们这里设置+1
        AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(10,1);

        System.out.println(" 原始stamp == " +atomicStampedReference.getStamp());

        new Thread(() -> {

            System.out.println("A 刚拿到的stamp == "+atomicStampedReference.getStamp());

            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }


            System.out.println(atomicStampedReference.compareAndSet(10, 15, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));
            System.out.println("A 第一次修改后的stamp == " +atomicStampedReference.getStamp());

            System.out.println(atomicStampedReference.compareAndSet(15, 10, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));
            System.out.println("A 第二次修改后的stamp == " +atomicStampedReference.getStamp());

            System.out.println("A 修改完后的stamp == " + atomicStampedReference.getStamp());

        },"A").start();


        new Thread(() -> {


            int stamp = atomicStampedReference.getStamp();
            System.out.println("B 刚拿到的stamp == "+stamp);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(atomicStampedReference.compareAndSet(10, 12,stamp, stamp+1));

            System.out.println("B 修改完后的stamp == " + atomicStampedReference.getStamp());
        },"B").start();
    }
}

执行结果:

在这里插入图片描述

因为在B线程CAS的时候,此时stamp的版本已经来到3了,而它所期望的stamp为1 ,所以这里即使它期望的值为10没有错,但因为乐观的存在,stamp没到到期望值,set不了 ,返回false

各种乐观的实现中通常都会用版本戳version来对记录或对象标记,避免并发操作带来的问题,在Java中,AtomicStampedReference也实现了这个作用,它通过包装[E,Integer]的元组来对对象标记版本戳stamp,从而避免ABA问题,例如下面的代码分别用AtomicInteger和AtomicStampedReference来对初始值为100的原子整型变量进行更新,AtomicInteger会成功执行CAS操作,而加上版本戳的AtomicStampedReference对于ABA问题会执行CAS失败

正确情况下:把B线程里面的CAS改为:

java">  System.out.println(atomicStampedReference.compareAndSet(10, 12,atomicStampedReference.getStamp(), atomicStampedReference.getStamp()+1));

执行结果:
在这里插入图片描述
在Java中,AtomicStampedReference类就实现了用版本号作比较额CAS机制。

1. java语言CAS底层如何实现?

利用unsafe提供的原子性操作方法。

2.什么事ABA问题?怎么解决?

当一个值从A变成B,又更新回A,普通CAS机制会误判通过检测。

利用版本号比较可以有效解决ABA问题。

这次的讨论就到这里,欢迎小伙伴们留言讨论!


http://www.niftyadmin.cn/n/1715646.html

相关文章

c++返回指针的下一个位置_Java经典算法:在每个节点中填充下一个右指针

给定以下完美的二叉树&#xff0c;1个/ 2 3///4 5 6 7调用函数后&#xff0c;树应如下所示&#xff1a;1->空/ 2-> 3->空///4-> 5-> 6-> 7->空Java解决方案1-简单公共 无效连接( TreeLinkNode根) {如果(根 null )返回;LinkedList < TreeLinkNode > …

和 远程文件夹同步_使用Unison 同步文件

Unison是一个跨平台的文件同步工具&#xff0c;它在同步计算机或存储设备之间的数据时非常有用。环境Centos7(1): 192.168.43.165Centos7(2): 192.168.43.166安装Unison在两个系统中都要安装epel源&#xff0c;然后都要安装unison&#xff1a;[root192_168_43_165 ~]# yum -y i…

8g内存和16g内存区别 mac_【双8G内存比16G内存速度快?详解双通道内存和磁盘阵列】...

前言&#xff1a;目前为止大多数游戏对于内存的需求是越来越高了&#xff0c;所以我在写单子的时候都尽可能的写8GX2这种组合&#xff0c;这时候就有很多人&#xff0c;而且几乎是全部的人&#xff0c;都会问我可不可以直接买单16G呢&#xff0c;所以我们今天来详细了解一下双8…

类的加载器以及类加载过程

1.类的加载图示 2.简述类的加载过程 当程序要使用某个类时&#xff0c;如果该类还未被加载到内存中&#xff0c;则系统会通过类的加载、类的链接、类的初始化这三个步骤来对类进行初始化。如果不出现意外&#xff0c;JVM将会连续完成这三个步骤&#xff0c;所以有时也把这三…

微信小程序装修解决方案ppt_如何制作微信小程序店铺?装修指南

商家要想吸引更多线上客户&#xff0c;做一个微信小程序店铺是很有用的。如何制作微信小程序店铺呢&#xff1f;教程如下&#xff1a;在「上线了」sxl.cn选择一个电商(或超级云名片-电商版)模板&#xff1a;进入后台编辑页面&#xff0c;你可以装修小程序主页&#xff0c;设置主…

坐稳了!带你入手一波双亲委派机制和沙箱安全机制。

1.双亲委派机制工作原理 1&#xff09;、如果一个类加载器收到了类加载请求&#xff0c;它并不会自己先去加载&#xff0c;而是把这个请求委托给父类的加载器去执行。 2&#xff09;、如果父类加载器还存在其父类加载器&#xff0c;则进一步向上委托&#xff0c;依次递归&…

cocos 报错dts文件未导入_CSV文件与MySQL表的导入、导出

一、将CSV文件导入MySQL表这里主要用到的是LOAD DATA INFILE语句在导入文件操作之前&#xff0c;需要准备以下内容&#xff1a;将要导入文件的数据对应的数据库表。准备好一个CSV文件&#xff0c;其数据与表的列数和每列中的数据类型相匹配。连接到MySQL数据库服务器的帐户具有…

vuex中的值变化 页面重新渲染_挑战全网最幽默的Vuex教程:第一讲 Vuex到底是什么鬼...

先说两句官方已经有教程了&#xff0c;为什么还要写这个教程呢&#xff1f;说实话&#xff0c;还真不是我闲着蛋疼&#xff0c;官方的教程真的是太官方了&#xff0c;对于刚入门 Vuex 的童鞋来说&#xff0c;想必看官方的教程&#xff0c;很多地方就如同看圣经一样&#xff0c;…